summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps
diff options
context:
space:
mode:
authorMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
committerMatt A. Tobin <mattatobin@localhost.localdomain>2018-02-02 04:16:08 -0500
commit5f8de423f190bbb79a62f804151bc24824fa32d8 (patch)
tree10027f336435511475e392454359edea8e25895d /toolkit/mozapps
parent49ee0794b5d912db1f95dce6eb52d781dc210db5 (diff)
downloadUXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.gz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.lz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.tar.xz
UXP-5f8de423f190bbb79a62f804151bc24824fa32d8.zip
Add m-esr52 at 52.6.0
Diffstat (limited to 'toolkit/mozapps')
-rw-r--r--toolkit/mozapps/downloads/DownloadLastDir.jsm195
-rw-r--r--toolkit/mozapps/downloads/DownloadPaths.jsm89
-rw-r--r--toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm399
-rw-r--r--toolkit/mozapps/downloads/DownloadUtils.jsm600
-rw-r--r--toolkit/mozapps/downloads/content/DownloadProgressListener.js117
-rw-r--r--toolkit/mozapps/downloads/content/download.xml327
-rw-r--r--toolkit/mozapps/downloads/content/downloads.css50
-rw-r--r--toolkit/mozapps/downloads/content/downloads.js1320
-rw-r--r--toolkit/mozapps/downloads/content/downloads.xul164
-rw-r--r--toolkit/mozapps/downloads/content/unknownContentType.xul107
-rw-r--r--toolkit/mozapps/downloads/jar.mn12
-rw-r--r--toolkit/mozapps/downloads/moz.build24
-rw-r--r--toolkit/mozapps/downloads/nsHelperAppDlg.js1147
-rw-r--r--toolkit/mozapps/downloads/nsHelperAppDlg.manifest2
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/chrome.ini10
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul117
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul108
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif1
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^1
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt1
-rw-r--r--toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^2
-rw-r--r--toolkit/mozapps/downloads/tests/moz.build8
-rw-r--r--toolkit/mozapps/downloads/tests/unit/.eslintrc.js7
-rw-r--r--toolkit/mozapps/downloads/tests/unit/head_downloads.js5
-rw-r--r--toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js131
-rw-r--r--toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js237
-rw-r--r--toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js55
-rw-r--r--toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js26
-rw-r--r--toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js25
-rw-r--r--toolkit/mozapps/downloads/tests/unit/xpcshell.ini10
-rw-r--r--toolkit/mozapps/extensions/.eslintrc.js8
-rw-r--r--toolkit/mozapps/extensions/AddonContentPolicy.cpp478
-rw-r--r--toolkit/mozapps/extensions/AddonContentPolicy.h22
-rw-r--r--toolkit/mozapps/extensions/AddonManager.jsm3674
-rw-r--r--toolkit/mozapps/extensions/AddonManagerWebAPI.cpp171
-rw-r--r--toolkit/mozapps/extensions/AddonManagerWebAPI.h33
-rw-r--r--toolkit/mozapps/extensions/AddonPathService.cpp258
-rw-r--r--toolkit/mozapps/extensions/AddonPathService.h55
-rw-r--r--toolkit/mozapps/extensions/ChromeManifestParser.jsm157
-rw-r--r--toolkit/mozapps/extensions/DeferredSave.jsm275
-rw-r--r--toolkit/mozapps/extensions/LightweightThemeManager.jsm909
-rw-r--r--toolkit/mozapps/extensions/addonManager.js296
-rw-r--r--toolkit/mozapps/extensions/amContentHandler.js100
-rw-r--r--toolkit/mozapps/extensions/amIAddonManager.idl29
-rw-r--r--toolkit/mozapps/extensions/amIAddonPathService.idl37
-rw-r--r--toolkit/mozapps/extensions/amIWebInstallListener.idl134
-rw-r--r--toolkit/mozapps/extensions/amIWebInstaller.idl82
-rw-r--r--toolkit/mozapps/extensions/amInstallTrigger.js240
-rw-r--r--toolkit/mozapps/extensions/amWebAPI.js269
-rw-r--r--toolkit/mozapps/extensions/amWebInstallListener.js348
-rw-r--r--toolkit/mozapps/extensions/content/OpenH264-license.txt59
-rw-r--r--toolkit/mozapps/extensions/content/about.js103
-rw-r--r--toolkit/mozapps/extensions/content/about.xul57
-rw-r--r--toolkit/mozapps/extensions/content/blocklist.css11
-rw-r--r--toolkit/mozapps/extensions/content/blocklist.js72
-rw-r--r--toolkit/mozapps/extensions/content/blocklist.xml58
-rw-r--r--toolkit/mozapps/extensions/content/blocklist.xul46
-rw-r--r--toolkit/mozapps/extensions/content/eula.js25
-rw-r--r--toolkit/mozapps/extensions/content/eula.xul35
-rw-r--r--toolkit/mozapps/extensions/content/extensions.css270
-rw-r--r--toolkit/mozapps/extensions/content/extensions.js3915
-rw-r--r--toolkit/mozapps/extensions/content/extensions.xml2008
-rw-r--r--toolkit/mozapps/extensions/content/extensions.xul715
-rw-r--r--toolkit/mozapps/extensions/content/gmpPrefs.xul8
-rw-r--r--toolkit/mozapps/extensions/content/list.js165
-rw-r--r--toolkit/mozapps/extensions/content/list.xul44
-rw-r--r--toolkit/mozapps/extensions/content/newaddon.js137
-rw-r--r--toolkit/mozapps/extensions/content/newaddon.xul67
-rw-r--r--toolkit/mozapps/extensions/content/pluginPrefs.xul20
-rw-r--r--toolkit/mozapps/extensions/content/setting.xml486
-rw-r--r--toolkit/mozapps/extensions/content/update.js663
-rw-r--r--toolkit/mozapps/extensions/content/update.xul194
-rw-r--r--toolkit/mozapps/extensions/content/updateinfo.xsl41
-rw-r--r--toolkit/mozapps/extensions/content/xpinstallConfirm.css8
-rw-r--r--toolkit/mozapps/extensions/content/xpinstallConfirm.js196
-rw-r--r--toolkit/mozapps/extensions/content/xpinstallConfirm.xul37
-rw-r--r--toolkit/mozapps/extensions/content/xpinstallItem.xml51
-rw-r--r--toolkit/mozapps/extensions/docs/SystemAddons.rst224
-rw-r--r--toolkit/mozapps/extensions/docs/index.rst14
-rw-r--r--toolkit/mozapps/extensions/extensions.manifest27
-rw-r--r--toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js39
-rw-r--r--toolkit/mozapps/extensions/internal/AddonConstants.jsm31
-rw-r--r--toolkit/mozapps/extensions/internal/AddonLogging.jsm192
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository.jsm1988
-rw-r--r--toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm522
-rw-r--r--toolkit/mozapps/extensions/internal/AddonTestUtils.jsm1232
-rw-r--r--toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm934
-rw-r--r--toolkit/mozapps/extensions/internal/Content.js38
-rw-r--r--toolkit/mozapps/extensions/internal/E10SAddonsRollout.jsm982
-rw-r--r--toolkit/mozapps/extensions/internal/GMPProvider.jsm699
-rw-r--r--toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm180
-rw-r--r--toolkit/mozapps/extensions/internal/PluginProvider.jsm600
-rw-r--r--toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm467
-rw-r--r--toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js17
-rw-r--r--toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js39
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProvider.jsm9305
-rw-r--r--toolkit/mozapps/extensions/internal/XPIProviderUtils.js2255
-rw-r--r--toolkit/mozapps/extensions/internal/moz.build36
-rw-r--r--toolkit/mozapps/extensions/jar.mn35
-rw-r--r--toolkit/mozapps/extensions/moz.build66
-rw-r--r--toolkit/mozapps/extensions/nsBlocklistService.js1678
-rw-r--r--toolkit/mozapps/extensions/nsBlocklistServiceContent.js113
-rw-r--r--toolkit/mozapps/extensions/test/AddonManagerTesting.jsm115
-rw-r--r--toolkit/mozapps/extensions/test/Makefile.in20
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_hard1_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_hard1_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_hard1_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_regexp1_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_regexp1_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_regexp1_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft1_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft1_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft1_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft2_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft2_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft2_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft3_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft3_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft3_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft4_1/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft4_2/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft4_3/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft5_1/install.rdf19
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft5_2/install.rdf19
-rw-r--r--toolkit/mozapps/extensions/test/addons/blocklist_soft5_3/install.rdf19
-rw-r--r--toolkit/mozapps/extensions/test/addons/bootstrap_globals/bootstrap.js29
-rw-r--r--toolkit/mozapps/extensions/test/addons/bootstrap_globals/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/min1max1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/min1max2/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/min1max3/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/min1max3b/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/override1x2-1x3/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_AddonRepository_1/install.rdf33
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_AddonRepository_2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/icon.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/preview.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm3
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm3
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm3
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap1_4/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap_const/bootstrap.js5
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bootstrap_const/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_a_1/install.rdf21
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_a_2/install.rdf21
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_b_1/install.rdf20
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_b_2/install.rdf20
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_c_1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_c_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_d_1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_d_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_e_1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_e_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_f_1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_f_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_g_1/install.rdf21
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug299716_g_2/install.rdf21
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_1/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_2/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_3/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_4/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_5/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_6/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_7/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_8/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug324121_9/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug335238_1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug335238_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug335238_3/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug335238_4/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug371495/install.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug394300_1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug394300_2/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf78
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug425657/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug470377_1/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug470377_2/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug470377_3/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug470377_4/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug470377_5/install.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug521905/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug567173/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug567184/bootstrap.js7
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug567184/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_2/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile20
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug594058/directory/file10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf21
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug595573/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug655254/install.rdf18
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug655254_2/bootstrap.js9
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug655254_2/install.rdf19
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug659772/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug675371/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug675371/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug675371/test.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug740612_1/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug740612_1/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug740612_2/bootstrap.js23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug740612_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_bug757663/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_cacheflush1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_cacheflush2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/chrome.manifest6
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/chrome.manifest7
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/chrome.manifest9
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/inner.jarbin0 -> 180 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/chrome.manifest6
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/components.manifest2
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/other/something.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/chrome.manifest7
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_data_directory/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_db_sanity_1_1/install.rdf58
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_db_sanity_1_2/install.rdf59
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/bootstrap.js10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_complete_webextension_v2/manifest.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/bootstrap.js10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_defer_webextension_v2/manifest.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/bootstrap.js8
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_webextension_v2/manifest.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary/dictionaries/ab-CD.dic2
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary_2/dictionaries/ab-CD.dic2
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary_3/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary_4/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_dictionary_5/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_distribution1_2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_experiment1/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf16
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_filepointer/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_getresource/icon.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install1/icon.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install1/icon64.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install1/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install2_1/icon.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install2_1/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install2_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install3/install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/addon4.xpibin0 -> 509 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/addon5.jarbin0 -> 512 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/addon6.xpibin0 -> 512 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/addon7.jarbin0 -> 512 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/badaddon.jar1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/badaddon.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/icon.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install4/install.rdf10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install5/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install5/install.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install6/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install7/install.rdf10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_install8/install.rdf10
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js17
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_langpack/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_langpack/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_locale/install.rdf61
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_locked2_5/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_locked2_6/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate4_6/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate4_7/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate6/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate7/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate8/chrome.manifest6
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate8/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_migrate9/install.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_symbol/bootstrap.js62
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_symbol/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_theme/install.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_theme/preview.png1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_undoincompatible/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_undoincompatible/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_undouninstall1/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_undouninstall1/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update12/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update8/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update_multi1/bootstrap.js5
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update_multi1/install.rdf16
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update_multi2/addon.xpibin0 -> 693 bytes
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_update_multi2/install.rdf9
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_updateid1/bootstrap.js5
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_updateid1/install.rdf16
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_updateid2/bootstrap.js5
-rw-r--r--toolkit/mozapps/extensions/test/addons/test_updateid2/install.rdf16
-rw-r--r--toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_1/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_2/install.rdf22
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_1/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_1/manifest.json14
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_3/_locales/en/messages.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_3/_locales/fr/messages.json10
-rw-r--r--toolkit/mozapps/extensions/test/addons/webextension_3/manifest.json12
-rw-r--r--toolkit/mozapps/extensions/test/browser/.eslintrc.js7
-rw-r--r--toolkit/mozapps/extensions/test/browser/addon_about.xul6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addon_prefs.xul6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1.xpibin0 -> 4426 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10.xpibin0 -> 4425 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2.xpibin0 -> 4427 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3.xpibin0 -> 4425 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4.xpibin0 -> 4432 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5.xpibin0 -> 4427 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6.xpibin0 -> 4424 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7.xpibin0 -> 4424 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1.xpibin0 -> 4427 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1.xpibin0 -> 4421 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1.xpibin0 -> 4425 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2.xpibin0 -> 4427 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1.xpibin0 -> 4449 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2.xpibin0 -> 4440 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1.xpibin0 -> 4424 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2.xpibin0 -> 4420 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_experiment1.xpibin0 -> 4328 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_experiment1/install.rdf16
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1.xpibin0 -> 5811 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/bootstrap.js8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/options.xul23
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/settings.dtd1
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom.xpibin0 -> 6155 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/binding.xml19
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/bootstrap.js8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/chrome.manifest2
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/options.xul5
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/string.dtd1
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info.xpibin0 -> 5279 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/bootstrap.js8
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/options.xul19
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_install1_1.xpibin0 -> 4489 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_install1_1/install.rdf32
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpibin0 -> 4415 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_install1_2/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl.xpibin0 -> 4430 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_installssl/install.rdf30
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpibin0 -> 4808 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_searching/bootstrap.js9
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_searching/install.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_1.xpibin0 -> 5479 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/bootstrap.js12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/frame-script.js6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_2.xpibin0 -> 5481 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/bootstrap.js12
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/chrome.manifest1
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/frame-script.js6
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/install.rdf31
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install.xpibin0 -> 4782 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/bootstrap.js9
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/install.rdf29
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed.xpibin0 -> 4560 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json11
-rw-r--r--toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html9
-rw-r--r--toolkit/mozapps/extensions/test/browser/blockNoPlugins.xml7
-rw-r--r--toolkit/mozapps/extensions/test/browser/blockPluginHard.xml11
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser-common.ini67
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser-window.ini52
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser.ini75
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js172
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_about.js84
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_addonrepository_performance.js99
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug523784.js120
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557943.js80
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557956.js524
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf310
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557956.xml20
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpibin0 -> 4438 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpibin0 -> 4426 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug562797.js975
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug562854.js129
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug562890.js78
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug562899.js88
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug562992.js70
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug567127.js136
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug567137.js40
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug570760.js44
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug572561.js99
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug573062.js116
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug577990.js132
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug580298.js98
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug581076.js132
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug586574.js286
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug587970.js180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug590347.js121
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug591465.js512
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug591465.xml35
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug591663.js161
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug593535.js119
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug593535.xml34
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug596336.js156
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug608316.js65
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug610764.js34
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug616841.js21
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug618502.js44
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug679604.js29
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_bug714593.js140
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js462
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js34
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_details.js1053
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_discovery.js651
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_discovery_install.js133
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_dragdrop.js234
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_eula.js85
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_eula.xml35
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_experiments.js654
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js63
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js418
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_hotfix.js171
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js680
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js207
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_inlinesettings_custom.js92
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js574
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_install.js312
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_install.rdf27
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_install.rdf^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_install.xml34
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_install1_3.xpibin0 -> 4419 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_installssl.js374
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_list.js956
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_manualupdates.js246
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_metadataTimeout.js114
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_newaddon.js232
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_openDialog.js173
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js124
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_pluginprefs.js61
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_purchase.js197
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_purchase.xml180
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_recentupdates.js125
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_searching.js698
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_searching.xml277
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_searching_empty.xml3
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sorting.js372
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_sorting_plugins.js95
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_system_addons_are_e10s.js13
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_tabsettings.js100
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_task_next_test.js17
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_types.js473
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_uninstalling.js1098
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_update.js53
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updateid.js84
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.js370
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf^headers^1
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi.js106
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_access.js127
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js174
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js62
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_install.js311
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js51
-rw-r--r--toolkit/mozapps/extensions/test/browser/browser_webext_options.js70
-rw-r--r--toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs43
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery.html10
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery_frame.html6
-rw-r--r--toolkit/mozapps/extensions/test/browser/discovery_install.html19
-rw-r--r--toolkit/mozapps/extensions/test/browser/head.js1468
-rw-r--r--toolkit/mozapps/extensions/test/browser/more_options.xul32
-rw-r--r--toolkit/mozapps/extensions/test/browser/moz.build10
-rw-r--r--toolkit/mozapps/extensions/test/browser/options.xul12
-rw-r--r--toolkit/mozapps/extensions/test/browser/plugin_test.html7
-rw-r--r--toolkit/mozapps/extensions/test/browser/redirect.sjs5
-rw-r--r--toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml15
-rw-r--r--toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/browser/signed_hotfix.xpibin0 -> 2745 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpibin0 -> 560 bytes
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html30
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html13
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul6
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checkframed.html7
-rw-r--r--toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html28
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/.eslintrc.js7
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/file_bug687194.xpibin0 -> 5659 bytes
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/file_empty.html2
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/mochitest.ini9
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_bug609794.html27
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_bug687194.html133
-rw-r--r--toolkit/mozapps/extensions/test/mochitest/test_bug887098.html52
-rw-r--r--toolkit/mozapps/extensions/test/moz.build19
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_change.xml31
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update1.rdf144
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update2.rdf144
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update3.rdf144
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/app_update.xml62
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update1.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update2.xml26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/manual_update.xml27
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml33
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpibin0 -> 633 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/empty.xpibin0 -> 197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/from_sources/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/from_sources/install.rdf28
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml45
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml5
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpibin0 -> 452 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js29
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js29
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf24
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_badid.xpibin0 -> 5151 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_broken.xpibin0 -> 5298 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_good.xpibin0 -> 5158 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_hash.xpibin0 -> 4471 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_plain.xpibin0 -> 4433 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_hash.xpibin0 -> 4474 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_plain.xpibin0 -> 4436 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_65_hash.xpibin0 -> 4487 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_badid.xpibin0 -> 6443 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_broken.xpibin0 -> 6563 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_signed.xpibin0 -> 6425 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_unsigned.xpibin0 -> 2436 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/preliminary_bootstrap_2.xpibin0 -> 5161 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_1.xpibin0 -> 5150 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_2.xpibin0 -> 5149 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_badid_2.xpibin0 -> 5155 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_2.xpibin0 -> 4627 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_badid_2.xpibin0 -> 4634 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_bootstrap_2.xpibin0 -> 1156 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_nonbootstrap_2.xpibin0 -> 691 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1.xpibin0 -> 4692 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1_badcert.xpibin0 -> 4808 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_2.xpibin0 -> 4695 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_1.xpibin0 -> 4692 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_2.xpibin0 -> 4695 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_3.xpibin0 -> 4697 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_1.xpibin0 -> 4689 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_2.xpibin0 -> 4691 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_3.xpibin0 -> 4693 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system4_1.xpibin0 -> 4692 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system5_1.xpibin0 -> 4691 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_1_unpack.xpibin0 -> 4708 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_2_notBootstrap.xpibin0 -> 4682 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_3_notMultiprocess.xpibin0 -> 4675 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete.xpibin0 -> 5090 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete_2.xpibin0 -> 4706 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer.xpibin0 -> 5095 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_2.xpibin0 -> 4701 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also.xpibin0 -> 5117 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also_2.xpibin0 -> 4716 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore.xpibin0 -> 5098 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpibin0 -> 4707 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_failed_update.xpibin0 -> 735 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml820
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.xml182
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_ignore.xml23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_normal.xml23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_strict.xml23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.xml3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_failed.xml21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml187
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.rdf70
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml28
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_regexp_1.xml20
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716.rdf181
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716_2.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug324121.rdf91
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml30
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug394300.rdf159
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug424262.xml185
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml333
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml208
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml15
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_1.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_2.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_3.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_4.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_5.rdf17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_1.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_2.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_3.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_4.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_5.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml10
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml4
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_1.xpibin0 -> 458 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_2.xpibin0 -> 458 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug541420.xpibin0 -> 577 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug542391.rdf25
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug554133.xml292
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug619730.xml7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_compatoverrides.xml228
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.rdf44
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_complete/bootstrap.js24
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_defer/bootstrap.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_ignore/bootstrap.js26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json11
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_dictionary.rdf65
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/bootstrap.js21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/install.rdf23
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/dummy.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/subdir2/dummy2.txt1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.xml304
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist2.xml31
-rwxr-xr-xtoolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml783
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.xml32
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_install.rdf63
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_install.xml53
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_migrate.rdf125
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_migrate4.rdf46
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json7
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml8
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml10
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_proxy/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml9
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml18
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_temporary/bootstrap.js1
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update.json215
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update.rdf270
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update.xml26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_update_multi.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json327
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf419
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/test_updateid.rdf26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpibin0 -> 452 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpibin0 -> 4182 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_addons.js1345
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/head_unpack.js3
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js625
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js704
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_compatmode.js90
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ChromeManifestParser.js108
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js549
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js598
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js244
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js299
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js66
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_addon_path_service.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js44
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js126
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js54
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_badschema.js404
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklist_gfx.js157
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklist_metadata_filters.js147
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklist_prefs.js148
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js120
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js1305
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js1403
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js17
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js37
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_resource.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug1180901.js35
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug1180901_2.js60
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug299716.js208
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug299716_2.js50
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug324121.js178
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug335238.js173
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug371495.js35
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug384052.js103
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug393285.js316
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug394300.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug397778.js117
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug406118.js155
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug424262.js62
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug425657.js27
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug430120.js135
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug449027.js429
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug455906.js517
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug465190.js39
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug468528.js58
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1_strictcompat.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_2.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3.js95
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3_strictcompat.js94
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug470377_4.js92
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug514327_1.js59
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug514327_2.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js139
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug521905.js59
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug526598.js54
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug541420.js37
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug542391.js464
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug554133.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js71
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js259
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js63
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug566626.js112
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug567184.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug569138.js147
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug570173.js61
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug576735.js66
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js174
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js88
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug595081.js27
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug596607.js147
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js26
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug619730.js64
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug620837.js145
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js164
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js340
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug675371.js91
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug740612.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug753900.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug757663.js112
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_bug953156.js51
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js82
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js127
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_checkCompatibility_themeOverride.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_checkcompatibility.js196
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js21
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js259
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js406
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js405
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_corruptfile.js83
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dataDirectory.js50
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js13
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_delay_update.js260
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js344
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js144
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dictionary.js811
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_disable.js194
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_distribution.js273
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_dss.js824
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_duplicateplugins.js187
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_e10s_restartless.js429
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_error.js90
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_experiment.js131
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js137
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js403
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_fuel.js165
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_general.js58
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_getresource.js94
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Device.js96
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_DriverNew.js92
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverNew.js123
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverOld.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_OK.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_DriverOld.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_OK.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_No_Comparison.js89
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OK.js94
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OS.js93
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_match.js95
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js95
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js96
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Vendor.js93
-rwxr-xr-xtoolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js145
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_prefs.js135
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js416
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js82
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js309
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_hotfix_cert.js167
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install.js1843
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_from_sources.js80
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js61
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js1726
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_isReady.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js372
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_langpack.js339
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_locale.js149
-rwxr-xr-xtoolkit/mozapps/extensions/test/xpcshell/test_locked.js544
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_locked2.js297
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js567
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_manifest.js562
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js347
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_metadata_update.js159
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js231
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js267
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js229
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js321
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate5.js139
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrateAddonRepository.js127
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_migrate_max_version.js103
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js120
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js98
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js107
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js66
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js200
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pass_symbol.js43
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_permissions.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js74
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js182
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js90
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pluginchange.js283
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_plugins.js210
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js221
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js99
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js64
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_proxies.js240
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_proxy.js106
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_registry.js158
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_reload.js235
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_safemode.js115
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js317
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_seen.js211
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_seen_newprofile.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js85
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js382
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js265
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js194
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_multi.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js136
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js234
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_softblocked.js109
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js66
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_startup.js932
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js203
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_switch_os.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js156
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js461
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js418
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_system_update.js788
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js146
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_temporary.js760
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_theme.js1139
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_types.js65
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_undothemeuninstall.js423
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js792
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js216
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update.js1398
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js138
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js184
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js107
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js1126
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js248
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js236
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_updateid.js86
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js206
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js209
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension.js421
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js306
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js169
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js478
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini334
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini12
-rw-r--r--toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini50
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/.eslintrc.js7
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/amosigned.xpibin0 -> 4420 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/amosigned2.xpibin0 -> 4421 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs21
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser.ini119
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js57
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js35
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth.js47
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js46
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js42
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js33
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js33
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js17
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cancel.js60
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_concurrent_installs.js127
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js30
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js44
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js43
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js37
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_empty.js28
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js29
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_enabled2.js32
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_enabled3.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_hash.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js39
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js39
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js39
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js40
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js83
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js35
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js52
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js44
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_offline.js62
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_relative.js55
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_multipackage.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_multiple.js72
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_naming.js67
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_tampered.js33
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_trigger.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_untrusted.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js34
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_switchtab.js49
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js41
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js56
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js57
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js38
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js35
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js53
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist2.js31
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js28
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js30
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist5.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist6.js25
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js32
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug540558.html23
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug638292.html17
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/bug645699.html31
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/concurrent_installs.html40
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs24
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi1
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/empty.xpibin0 -> 197 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/enabled.html24
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs15
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/head.js434
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/incompatible.xpibin0 -> 4442 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installchrome.html22
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installtrigger.html44
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html29
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/multipackage.xpibin0 -> 9589 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/navigate.html26
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/redirect.sjs45
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpibin0 -> 528 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/restartless.xpibin0 -> 4447 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed-multipackage.xpibin0 -> 2976 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed-no-cn.xpibin0 -> 2241 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed-no-o.xpibin0 -> 2247 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed-tampered.xpibin0 -> 2260 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed-untrusted.xpibin0 -> 2237 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed.xpibin0 -> 2250 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/signed2.xpibin0 -> 2938 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs101
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html20
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/theme.xpibin0 -> 4450 bytes
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html36
-rw-r--r--toolkit/mozapps/extensions/test/xpinstall/unsigned.xpibin0 -> 452 bytes
-rw-r--r--toolkit/mozapps/handling/content/dialog.js278
-rw-r--r--toolkit/mozapps/handling/content/dialog.xul52
-rw-r--r--toolkit/mozapps/handling/content/handler.css11
-rw-r--r--toolkit/mozapps/handling/content/handler.xml28
-rw-r--r--toolkit/mozapps/handling/jar.mn10
-rw-r--r--toolkit/mozapps/handling/moz.build12
-rw-r--r--toolkit/mozapps/handling/nsContentDispatchChooser.js85
-rw-r--r--toolkit/mozapps/handling/nsContentDispatchChooser.manifest2
-rw-r--r--toolkit/mozapps/installer/find-dupes.py135
-rw-r--r--toolkit/mozapps/installer/informulate.py46
-rw-r--r--toolkit/mozapps/installer/js-compare-ast.js28
-rw-r--r--toolkit/mozapps/installer/l10n-repack.py60
-rw-r--r--toolkit/mozapps/installer/linux/rpm/mozilla.desktop21
-rw-r--r--toolkit/mozapps/installer/linux/rpm/mozilla.spec134
-rw-r--r--toolkit/mozapps/installer/make-eme.mk16
-rw-r--r--toolkit/mozapps/installer/package-name.mk168
-rw-r--r--toolkit/mozapps/installer/packager.mk250
-rw-r--r--toolkit/mozapps/installer/packager.py415
-rw-r--r--toolkit/mozapps/installer/precompile_cache.js87
-rw-r--r--toolkit/mozapps/installer/signing.mk41
-rw-r--r--toolkit/mozapps/installer/strip.py23
-rw-r--r--toolkit/mozapps/installer/unpack.py22
-rw-r--r--toolkit/mozapps/installer/upload-files-APK.mk141
-rw-r--r--toolkit/mozapps/installer/upload-files.mk529
-rwxr-xr-xtoolkit/mozapps/installer/windows/nsis/common.nsh8024
-rw-r--r--toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh681
-rw-r--r--toolkit/mozapps/installer/windows/nsis/locale-rtl.nlf12
-rw-r--r--toolkit/mozapps/installer/windows/nsis/locale.nlf12
-rwxr-xr-xtoolkit/mozapps/installer/windows/nsis/locales.nsi23
-rw-r--r--toolkit/mozapps/installer/windows/nsis/makensis.mk100
-rwxr-xr-xtoolkit/mozapps/installer/windows/nsis/overrides.nsh604
-rw-r--r--toolkit/mozapps/installer/windows/nsis/preprocess-locale.py360
-rw-r--r--toolkit/mozapps/installer/windows/nsis/setup.icobin0 -> 25214 bytes
-rw-r--r--toolkit/mozapps/preferences/changemp.js237
-rw-r--r--toolkit/mozapps/preferences/changemp.xul67
-rw-r--r--toolkit/mozapps/preferences/fontbuilder.js126
-rw-r--r--toolkit/mozapps/preferences/jar.mn11
-rw-r--r--toolkit/mozapps/preferences/moz.build7
-rw-r--r--toolkit/mozapps/preferences/removemp.js56
-rw-r--r--toolkit/mozapps/preferences/removemp.xul46
-rw-r--r--toolkit/mozapps/update/UpdateTelemetry.jsm488
-rw-r--r--toolkit/mozapps/update/common-standalone/moz.build12
-rw-r--r--toolkit/mozapps/update/common/certificatecheck.cpp250
-rw-r--r--toolkit/mozapps/update/common/certificatecheck.h22
-rw-r--r--toolkit/mozapps/update/common/errors.h110
-rw-r--r--toolkit/mozapps/update/common/moz.build32
-rw-r--r--toolkit/mozapps/update/common/pathhash.cpp139
-rw-r--r--toolkit/mozapps/update/common/pathhash.h19
-rw-r--r--toolkit/mozapps/update/common/readstrings.cpp236
-rw-r--r--toolkit/mozapps/update/common/readstrings.h43
-rw-r--r--toolkit/mozapps/update/common/registrycertificates.cpp154
-rw-r--r--toolkit/mozapps/update/common/registrycertificates.h14
-rw-r--r--toolkit/mozapps/update/common/sources.mozbuild28
-rw-r--r--toolkit/mozapps/update/common/uachelper.cpp222
-rw-r--r--toolkit/mozapps/update/common/uachelper.h23
-rw-r--r--toolkit/mozapps/update/common/updatecommon.cpp213
-rw-r--r--toolkit/mozapps/update/common/updatecommon.h47
-rw-r--r--toolkit/mozapps/update/common/updatedefines.h142
-rw-r--r--toolkit/mozapps/update/common/updatehelper.cpp609
-rw-r--r--toolkit/mozapps/update/common/updatehelper.h29
-rw-r--r--toolkit/mozapps/update/common/win_dirent.h32
-rw-r--r--toolkit/mozapps/update/content/history.js70
-rw-r--r--toolkit/mozapps/update/content/history.xul39
-rw-r--r--toolkit/mozapps/update/content/updates.css33
-rw-r--r--toolkit/mozapps/update/content/updates.js1399
-rw-r--r--toolkit/mozapps/update/content/updates.xml83
-rw-r--r--toolkit/mozapps/update/content/updates.xul206
-rw-r--r--toolkit/mozapps/update/jar.mn12
-rw-r--r--toolkit/mozapps/update/moz.build33
-rw-r--r--toolkit/mozapps/update/nsIUpdateService.idl575
-rw-r--r--toolkit/mozapps/update/nsUpdateService.js4567
-rw-r--r--toolkit/mozapps/update/nsUpdateService.manifest12
-rw-r--r--toolkit/mozapps/update/nsUpdateServiceStub.js45
-rw-r--r--toolkit/mozapps/update/tests/Makefile.in39
-rw-r--r--toolkit/mozapps/update/tests/TestAUSHelper.cpp423
-rw-r--r--toolkit/mozapps/update/tests/TestAUSReadStrings.cpp172
-rw-r--r--toolkit/mozapps/update/tests/TestAUSReadStrings1.ini47
-rw-r--r--toolkit/mozapps/update/tests/TestAUSReadStrings2.ini39
-rw-r--r--toolkit/mozapps/update/tests/TestAUSReadStrings3.ini39
-rw-r--r--toolkit/mozapps/update/tests/chrome/.eslintrc.js7
-rw-r--r--toolkit/mozapps/update/tests/chrome/chrome.ini64
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul50
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul51
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul55
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul46
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul46
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul53
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul55
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul55
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul53
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul52
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul67
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul68
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul94
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul55
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul60
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul60
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul66
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul65
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul65
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul46
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul49
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul50
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul61
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul58
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul50
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul96
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul50
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul44
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul64
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul63
-rw-r--r--toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul112
-rw-r--r--toolkit/mozapps/update/tests/chrome/update.sjs194
-rw-r--r--toolkit/mozapps/update/tests/chrome/utils.js1011
-rw-r--r--toolkit/mozapps/update/tests/data/complete.exebin0 -> 79872 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete.marbin0 -> 97888 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete.pngbin0 -> 878 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete_log_success_mac332
-rw-r--r--toolkit/mozapps/update/tests/data/complete_log_success_win320
-rw-r--r--toolkit/mozapps/update/tests/data/complete_mac.marbin0 -> 98454 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete_precomplete18
-rw-r--r--toolkit/mozapps/update/tests/data/complete_precomplete_mac21
-rw-r--r--toolkit/mozapps/update/tests/data/complete_removed-files41
-rw-r--r--toolkit/mozapps/update/tests/data/complete_removed-files_mac41
-rw-r--r--toolkit/mozapps/update/tests/data/complete_update_manifest59
-rw-r--r--toolkit/mozapps/update/tests/data/old_version.marbin0 -> 421 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial.exebin0 -> 79872 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial.marbin0 -> 10645 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial.pngbin0 -> 776 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_failure_mac192
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_failure_win192
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_success_mac279
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_success_win279
-rw-r--r--toolkit/mozapps/update/tests/data/partial_mac.marbin0 -> 11172 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial_precomplete19
-rw-r--r--toolkit/mozapps/update/tests/data/partial_precomplete_mac22
-rw-r--r--toolkit/mozapps/update/tests/data/partial_removed-files41
-rw-r--r--toolkit/mozapps/update/tests/data/partial_removed-files_mac41
-rw-r--r--toolkit/mozapps/update/tests/data/partial_update_manifest63
-rw-r--r--toolkit/mozapps/update/tests/data/replace_log_success6
-rw-r--r--toolkit/mozapps/update/tests/data/shared.js632
-rw-r--r--toolkit/mozapps/update/tests/data/sharedUpdateXML.js364
-rw-r--r--toolkit/mozapps/update/tests/data/simple.marbin0 -> 1031 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/wrong_product_channel.marbin0 -> 421 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js53
-rw-r--r--toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js4047
-rw-r--r--toolkit/mozapps/update/tests/moz.build101
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js7
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js138
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js46
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js29
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js30
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js35
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js45
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js161
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js66
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js225
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/head_update.js8
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js285
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js75
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js76
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js74
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js177
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js305
-rw-r--r--toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini27
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js7
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/head_update.js8
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js45
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js47
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js44
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js43
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js41
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js83
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js88
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js82
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js65
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js70
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js113
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js62
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js61
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js61
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js48
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js48
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js47
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js67
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js67
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js57
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js56
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js71
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js70
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js72
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js71
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js62
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js41
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js132
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js112
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js96
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js79
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js51
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js51
-rw-r--r--toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini136
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js7
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js35
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js39
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/head_update.js8
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js47
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js44
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js43
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js38
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js37
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js41
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js83
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js82
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js65
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js70
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js62
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js61
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js61
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js48
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js48
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js47
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js67
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js67
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js57
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js56
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js71
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js70
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js72
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js71
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js63
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js62
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js41
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js132
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js112
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js96
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js79
-rw-r--r--toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini156
-rw-r--r--toolkit/mozapps/update/updater/Launchd.plist10
-rw-r--r--toolkit/mozapps/update/updater/Makefile.in29
-rw-r--r--toolkit/mozapps/update/updater/archivereader.cpp324
-rw-r--r--toolkit/mozapps/update/updater/archivereader.h41
-rw-r--r--toolkit/mozapps/update/updater/automounter_gonk.cpp251
-rw-r--r--toolkit/mozapps/update/updater/automounter_gonk.h48
-rw-r--r--toolkit/mozapps/update/updater/bspatch.cpp187
-rw-r--r--toolkit/mozapps/update/updater/bspatch.h93
-rw-r--r--toolkit/mozapps/update/updater/dep1.derbin0 -> 671 bytes
-rw-r--r--toolkit/mozapps/update/updater/dep2.derbin0 -> 671 bytes
-rw-r--r--toolkit/mozapps/update/updater/gen_cert_header.py22
-rw-r--r--toolkit/mozapps/update/updater/launchchild_osx.mm384
-rw-r--r--toolkit/mozapps/update/updater/loaddlls.cpp103
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in39
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo1
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in7
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib19
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib22
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nibbin0 -> 5567 bytes
-rw-r--r--toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icnsbin0 -> 55969 bytes
-rw-r--r--toolkit/mozapps/update/updater/module.ver1
-rw-r--r--toolkit/mozapps/update/updater/moz.build62
-rw-r--r--toolkit/mozapps/update/updater/nightly_aurora_level3_primary.derbin0 -> 679 bytes
-rw-r--r--toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.derbin0 -> 679 bytes
-rw-r--r--toolkit/mozapps/update/updater/progressui.h40
-rw-r--r--toolkit/mozapps/update/updater/progressui_gonk.cpp53
-rw-r--r--toolkit/mozapps/update/updater/progressui_gtk.cpp132
-rw-r--r--toolkit/mozapps/update/updater/progressui_null.cpp25
-rw-r--r--toolkit/mozapps/update/updater/progressui_osx.mm144
-rw-r--r--toolkit/mozapps/update/updater/progressui_win.cpp319
-rw-r--r--toolkit/mozapps/update/updater/release_primary.derbin0 -> 709 bytes
-rw-r--r--toolkit/mozapps/update/updater/release_secondary.derbin0 -> 713 bytes
-rw-r--r--toolkit/mozapps/update/updater/resource.h29
-rw-r--r--toolkit/mozapps/update/updater/updater-common.build136
-rw-r--r--toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in41
-rw-r--r--toolkit/mozapps/update/updater/updater-xpcshell/moz.build14
-rw-r--r--toolkit/mozapps/update/updater/updater.cpp4454
-rw-r--r--toolkit/mozapps/update/updater/updater.exe.comctl32.manifest38
-rw-r--r--toolkit/mozapps/update/updater/updater.exe.manifest26
-rw-r--r--toolkit/mozapps/update/updater/updater.icobin0 -> 92854 bytes
-rw-r--r--toolkit/mozapps/update/updater/updater.pngbin0 -> 4030 bytes
-rw-r--r--toolkit/mozapps/update/updater/updater.rc137
-rw-r--r--toolkit/mozapps/update/updater/win_dirent.cpp78
-rw-r--r--toolkit/mozapps/update/updater/xpcshellCertificate.derbin0 -> 677 bytes
1326 files changed, 180503 insertions, 0 deletions
diff --git a/toolkit/mozapps/downloads/DownloadLastDir.jsm b/toolkit/mozapps/downloads/DownloadLastDir.jsm
new file mode 100644
index 000000000..552fd3ef1
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadLastDir.jsm
@@ -0,0 +1,195 @@
+/* -*- 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/. */
+
+/*
+ * The behavior implemented by gDownloadLastDir is documented here.
+ *
+ * In normal browsing sessions, gDownloadLastDir uses the browser.download.lastDir
+ * preference to store the last used download directory. The first time the user
+ * switches into the private browsing mode, the last download directory is
+ * preserved to the pref value, but if the user switches to another directory
+ * during the private browsing mode, that directory is not stored in the pref,
+ * and will be merely kept in memory. When leaving the private browsing mode,
+ * this in-memory value will be discarded, and the last download directory
+ * will be reverted to the pref value.
+ *
+ * Both the pref and the in-memory value will be cleared when clearing the
+ * browsing history. This effectively changes the last download directory
+ * to the default download directory on each platform.
+ *
+ * If passed a URI, the last used directory is also stored with that URI in the
+ * content preferences database. This can be disabled by setting the pref
+ * browser.download.lastDir.savePerSite to false.
+ */
+
+const LAST_DIR_PREF = "browser.download.lastDir";
+const SAVE_PER_SITE_PREF = LAST_DIR_PREF + ".savePerSite";
+const nsIFile = Components.interfaces.nsIFile;
+
+this.EXPORTED_SYMBOLS = [ "DownloadLastDir" ];
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/PrivateBrowsingUtils.jsm");
+
+var observer = {
+ QueryInterface: function (aIID) {
+ if (aIID.equals(Components.interfaces.nsIObserver) ||
+ aIID.equals(Components.interfaces.nsISupports) ||
+ aIID.equals(Components.interfaces.nsISupportsWeakReference))
+ return this;
+ throw Components.results.NS_NOINTERFACE;
+ },
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "last-pb-context-exited":
+ gDownloadLastDirFile = null;
+ break;
+ case "browser:purge-session-history":
+ gDownloadLastDirFile = null;
+ if (Services.prefs.prefHasUserValue(LAST_DIR_PREF))
+ Services.prefs.clearUserPref(LAST_DIR_PREF);
+ // Ensure that purging session history causes both the session-only PB cache
+ // and persistent prefs to be cleared.
+ let cps2 = Components.classes["@mozilla.org/content-pref/service;1"].
+ getService(Components.interfaces.nsIContentPrefService2);
+
+ cps2.removeByName(LAST_DIR_PREF, {usePrivateBrowsing: false});
+ cps2.removeByName(LAST_DIR_PREF, {usePrivateBrowsing: true});
+ break;
+ }
+ }
+};
+
+var os = Components.classes["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService);
+os.addObserver(observer, "last-pb-context-exited", true);
+os.addObserver(observer, "browser:purge-session-history", true);
+
+function readLastDirPref() {
+ try {
+ return Services.prefs.getComplexValue(LAST_DIR_PREF, nsIFile);
+ }
+ catch (e) {
+ return null;
+ }
+}
+
+function isContentPrefEnabled() {
+ try {
+ return Services.prefs.getBoolPref(SAVE_PER_SITE_PREF);
+ }
+ catch (e) {
+ return true;
+ }
+}
+
+var gDownloadLastDirFile = readLastDirPref();
+
+this.DownloadLastDir = function DownloadLastDir(aWindow) {
+ let loadContext = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIWebNavigation)
+ .QueryInterface(Components.interfaces.nsILoadContext);
+ // Need this in case the real thing has gone away by the time we need it.
+ // We only care about the private browsing state. All the rest of the
+ // load context isn't of interest to the content pref service.
+ this.fakeContext = {
+ QueryInterface: XPCOMUtils.generateQI([Components.interfaces.nsILoadContext]),
+ usePrivateBrowsing: loadContext.usePrivateBrowsing,
+ originAttributes: {},
+ };
+}
+
+DownloadLastDir.prototype = {
+ isPrivate: function DownloadLastDir_isPrivate() {
+ return this.fakeContext.usePrivateBrowsing;
+ },
+ // compat shims
+ get file() { return this._getLastFile(); },
+ set file(val) { this.setFile(null, val); },
+ cleanupPrivateFile: function () {
+ gDownloadLastDirFile = null;
+ },
+ // This function is now deprecated as it uses the sync nsIContentPrefService
+ // interface. New consumers should use the getFileAsync function.
+ getFile: function (aURI) {
+ let Deprecated = Components.utils.import("resource://gre/modules/Deprecated.jsm", {}).Deprecated;
+ Deprecated.warning("DownloadLastDir.getFile is deprecated. Please use getFileAsync instead.",
+ "https://developer.mozilla.org/en-US/docs/Mozilla/JavaScript_code_modules/DownloadLastDir.jsm",
+ Components.stack.caller);
+
+ if (aURI && isContentPrefEnabled()) {
+ let lastDir = Services.contentPrefs.getPref(aURI, LAST_DIR_PREF, this.fakeContext);
+ if (lastDir) {
+ var lastDirFile = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsIFile);
+ lastDirFile.initWithPath(lastDir);
+ return lastDirFile;
+ }
+ }
+ return this._getLastFile();
+ },
+
+ _getLastFile: function () {
+ if (gDownloadLastDirFile && !gDownloadLastDirFile.exists())
+ gDownloadLastDirFile = null;
+
+ if (this.isPrivate()) {
+ if (!gDownloadLastDirFile)
+ gDownloadLastDirFile = readLastDirPref();
+ return gDownloadLastDirFile;
+ }
+ return readLastDirPref();
+ },
+
+ getFileAsync: function(aURI, aCallback) {
+ let plainPrefFile = this._getLastFile();
+ if (!aURI || !isContentPrefEnabled()) {
+ Services.tm.mainThread.dispatch(() => aCallback(plainPrefFile),
+ Components.interfaces.nsIThread.DISPATCH_NORMAL);
+ return;
+ }
+
+ let uri = aURI instanceof Components.interfaces.nsIURI ? aURI.spec : aURI;
+ let cps2 = Components.classes["@mozilla.org/content-pref/service;1"]
+ .getService(Components.interfaces.nsIContentPrefService2);
+ let result = null;
+ cps2.getByDomainAndName(uri, LAST_DIR_PREF, this.fakeContext, {
+ handleResult: aResult => result = aResult,
+ handleCompletion: function(aReason) {
+ let file = plainPrefFile;
+ if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK &&
+ result instanceof Components.interfaces.nsIContentPref) {
+ file = Components.classes["@mozilla.org/file/local;1"]
+ .createInstance(Components.interfaces.nsIFile);
+ file.initWithPath(result.value);
+ }
+ aCallback(file);
+ }
+ });
+ },
+
+ setFile: function (aURI, aFile) {
+ if (aURI && isContentPrefEnabled()) {
+ let uri = aURI instanceof Components.interfaces.nsIURI ? aURI.spec : aURI;
+ let cps2 = Components.classes["@mozilla.org/content-pref/service;1"]
+ .getService(Components.interfaces.nsIContentPrefService2);
+ if (aFile instanceof Components.interfaces.nsIFile)
+ cps2.set(uri, LAST_DIR_PREF, aFile.path, this.fakeContext);
+ else
+ cps2.removeByDomainAndName(uri, LAST_DIR_PREF, this.fakeContext);
+ }
+ if (this.isPrivate()) {
+ if (aFile instanceof Components.interfaces.nsIFile)
+ gDownloadLastDirFile = aFile.clone();
+ else
+ gDownloadLastDirFile = null;
+ } else if (aFile instanceof Components.interfaces.nsIFile) {
+ Services.prefs.setComplexValue(LAST_DIR_PREF, nsIFile, aFile);
+ } else if (Services.prefs.prefHasUserValue(LAST_DIR_PREF)) {
+ Services.prefs.clearUserPref(LAST_DIR_PREF);
+ }
+ }
+};
diff --git a/toolkit/mozapps/downloads/DownloadPaths.jsm b/toolkit/mozapps/downloads/DownloadPaths.jsm
new file mode 100644
index 000000000..6ca6dfc13
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadPaths.jsm
@@ -0,0 +1,89 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* 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.EXPORTED_SYMBOLS = [
+ "DownloadPaths",
+];
+
+/**
+ * This module provides the DownloadPaths object which contains methods for
+ * giving names and paths to files being downloaded.
+ *
+ * List of methods:
+ *
+ * nsILocalFile
+ * createNiceUniqueFile(nsILocalFile aLocalFile)
+ *
+ * [string base, string ext]
+ * splitBaseNameAndExtension(string aLeafName)
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+this.DownloadPaths = {
+ /**
+ * Creates a uniquely-named file starting from the name of the provided file.
+ * If a file with the provided name already exists, the function attempts to
+ * create nice alternatives, like "base(1).ext" (instead of "base-1.ext").
+ *
+ * If a unique name cannot be found, the function throws the XPCOM exception
+ * NS_ERROR_FILE_TOO_BIG. Other exceptions, like NS_ERROR_FILE_ACCESS_DENIED,
+ * can also be expected.
+ *
+ * @param aTemplateFile
+ * nsILocalFile whose leaf name is going to be used as a template. The
+ * provided object is not modified.
+ * @returns A new instance of an nsILocalFile object pointing to the newly
+ * created empty file. On platforms that support permission bits, the
+ * file is created with permissions 644.
+ */
+ createNiceUniqueFile: function DP_createNiceUniqueFile(aTemplateFile) {
+ // Work on a clone of the provided template file object.
+ var curFile = aTemplateFile.clone().QueryInterface(Ci.nsILocalFile);
+ var [base, ext] = DownloadPaths.splitBaseNameAndExtension(curFile.leafName);
+ // Try other file names, for example "base(1).txt" or "base(1).tar.gz",
+ // only if the file name initially set already exists.
+ for (let i = 1; i < 10000 && curFile.exists(); i++) {
+ curFile.leafName = base + "(" + i + ")" + ext;
+ }
+ // At this point we hand off control to createUnique, which will create the
+ // file with the name we chose, if it is valid. If not, createUnique will
+ // attempt to modify it again, for example it will shorten very long names
+ // that can't be created on some platforms, and for which a normal call to
+ // nsIFile.create would result in NS_ERROR_FILE_NOT_FOUND. This can result
+ // very rarely in strange names like "base(9999).tar-1.gz" or "ba-1.gz".
+ curFile.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+ return curFile;
+ },
+
+ /**
+ * Separates the base name from the extension in a file name, recognizing some
+ * double extensions like ".tar.gz".
+ *
+ * @param aLeafName
+ * The full leaf name to be parsed. Be careful when processing names
+ * containing leading or trailing dots or spaces.
+ * @returns [base, ext]
+ * The base name of the file, which can be empty, and its extension,
+ * which always includes the leading dot unless it's an empty string.
+ * Concatenating the two items always results in the original name.
+ */
+ splitBaseNameAndExtension: function DP_splitBaseNameAndExtension(aLeafName) {
+ // The following regular expression is built from these key parts:
+ // .*? Matches the base name non-greedily.
+ // \.[A-Z0-9]{1,3} Up to three letters or numbers preceding a
+ // double extension.
+ // \.(?:gz|bz2|Z) The second part of common double extensions.
+ // \.[^.]* Matches any extension or a single trailing dot.
+ var [, base, ext] = /(.*?)(\.[A-Z0-9]{1,3}\.(?:gz|bz2|Z)|\.[^.]*)?$/i
+ .exec(aLeafName);
+ // Return an empty string instead of undefined if no extension is found.
+ return [base, ext || ""];
+ }
+};
diff --git a/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm b/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm
new file mode 100644
index 000000000..bccbeda56
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadTaskbarProgress.jsm
@@ -0,0 +1,399 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*-
+ * vim: sw=2 ts=2 sts=2 et filetype=javascript
+ * 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.EXPORTED_SYMBOLS = [
+ "DownloadTaskbarProgress",
+];
+
+// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+const Cr = Components.results;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Services",
+ "resource://gre/modules/Services.jsm");
+
+const kTaskbarIDWin = "@mozilla.org/windows-taskbar;1";
+const kTaskbarIDMac = "@mozilla.org/widget/macdocksupport;1";
+
+// DownloadTaskbarProgress Object
+
+this.DownloadTaskbarProgress =
+{
+ init: function DTP_init()
+ {
+ if (DownloadTaskbarProgressUpdater) {
+ DownloadTaskbarProgressUpdater._init();
+ }
+ },
+
+ /**
+ * Called when a browser window appears. This has an effect only when we
+ * don't already have an active window.
+ *
+ * @param aWindow
+ * The browser window that we'll potentially use to display the
+ * progress.
+ */
+ onBrowserWindowLoad: function DTP_onBrowserWindowLoad(aWindow)
+ {
+ this.init();
+ if (!DownloadTaskbarProgressUpdater) {
+ return;
+ }
+ if (!DownloadTaskbarProgressUpdater._activeTaskbarProgress) {
+ DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, false);
+ }
+ },
+
+ /**
+ * Called when the download window appears. The download window will take
+ * over as the active window.
+ */
+ onDownloadWindowLoad: function DTP_onDownloadWindowLoad(aWindow)
+ {
+ if (!DownloadTaskbarProgressUpdater) {
+ return;
+ }
+ DownloadTaskbarProgressUpdater._setActiveWindow(aWindow, true);
+ },
+
+ /**
+ * Getters for internal DownloadTaskbarProgressUpdater values
+ */
+
+ get activeTaskbarProgress() {
+ if (!DownloadTaskbarProgressUpdater) {
+ return null;
+ }
+ return DownloadTaskbarProgressUpdater._activeTaskbarProgress;
+ },
+
+ get activeWindowIsDownloadWindow() {
+ if (!DownloadTaskbarProgressUpdater) {
+ return null;
+ }
+ return DownloadTaskbarProgressUpdater._activeWindowIsDownloadWindow;
+ },
+
+ get taskbarState() {
+ if (!DownloadTaskbarProgressUpdater) {
+ return null;
+ }
+ return DownloadTaskbarProgressUpdater._taskbarState;
+ },
+
+};
+
+// DownloadTaskbarProgressUpdater Object
+
+var DownloadTaskbarProgressUpdater =
+{
+ // / Whether the taskbar is initialized.
+ _initialized: false,
+
+ // / Reference to the taskbar.
+ _taskbar: null,
+
+ // / Reference to the download manager.
+ _dm: null,
+
+ /**
+ * Initialize and register ourselves as a download progress listener.
+ */
+ _init: function DTPU_init()
+ {
+ if (this._initialized) {
+ return; // Already initialized
+ }
+ this._initialized = true;
+
+ if (kTaskbarIDWin in Cc) {
+ this._taskbar = Cc[kTaskbarIDWin].getService(Ci.nsIWinTaskbar);
+ if (!this._taskbar.available) {
+ // The Windows version is probably too old
+ DownloadTaskbarProgressUpdater = null;
+ return;
+ }
+ } else if (kTaskbarIDMac in Cc) {
+ this._activeTaskbarProgress = Cc[kTaskbarIDMac].
+ getService(Ci.nsITaskbarProgress);
+ } else {
+ DownloadTaskbarProgressUpdater = null;
+ return;
+ }
+
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
+
+ this._dm = Cc["@mozilla.org/download-manager;1"].
+ getService(Ci.nsIDownloadManager);
+ this._dm.addPrivacyAwareListener(this);
+
+ this._os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ this._os.addObserver(this, "quit-application-granted", false);
+
+ this._updateStatus();
+ // onBrowserWindowLoad/onDownloadWindowLoad are going to set the active
+ // window, so don't do it here.
+ },
+
+ /**
+ * Unregisters ourselves as a download progress listener.
+ */
+ _uninit: function DTPU_uninit() {
+ this._dm.removeListener(this);
+ this._os.removeObserver(this, "quit-application-granted");
+ this._activeTaskbarProgress = null;
+ this._initialized = false;
+ },
+
+ /**
+ * This holds a reference to the taskbar progress for the window we're
+ * working with. This window would preferably be download window, but can be
+ * another window if it isn't open.
+ */
+ _activeTaskbarProgress: null,
+
+ // / Whether the active window is the download window
+ _activeWindowIsDownloadWindow: false,
+
+ /**
+ * Sets the active window, and whether it's the download window. This takes
+ * care of clearing out the previous active window's taskbar item, updating
+ * the taskbar, and setting an onunload listener.
+ *
+ * @param aWindow
+ * The window to set as active.
+ * @param aIsDownloadWindow
+ * Whether this window is a download window.
+ */
+ _setActiveWindow: function DTPU_setActiveWindow(aWindow, aIsDownloadWindow)
+ {
+ if (AppConstants.platform == "win") {
+ // Clear out the taskbar for the old active window. (If there was no active
+ // window, this is a no-op.)
+ this._clearTaskbar();
+
+ this._activeWindowIsDownloadWindow = aIsDownloadWindow;
+ if (aWindow) {
+ // Get the taskbar progress for this window
+ let docShell = aWindow.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIXULWindow).docShell;
+ let taskbarProgress = this._taskbar.getTaskbarProgress(docShell);
+ this._activeTaskbarProgress = taskbarProgress;
+
+ this._updateTaskbar();
+ // _onActiveWindowUnload is idempotent, so we don't need to check whether
+ // we've already set this before or not.
+ aWindow.addEventListener("unload", function () {
+ DownloadTaskbarProgressUpdater._onActiveWindowUnload(taskbarProgress);
+ }, false);
+ }
+ else {
+ this._activeTaskbarProgress = null;
+ }
+ }
+ },
+
+ // / Current state displayed on the active window's taskbar item
+ _taskbarState: null,
+ _totalSize: 0,
+ _totalTransferred: 0,
+
+ _shouldSetState: function DTPU_shouldSetState()
+ {
+ if (AppConstants.platform == "win") {
+ // If the active window is not the download manager window, set the state
+ // only if it is normal or indeterminate.
+ return this._activeWindowIsDownloadWindow ||
+ (this._taskbarState == Ci.nsITaskbarProgress.STATE_NORMAL ||
+ this._taskbarState == Ci.nsITaskbarProgress.STATE_INDETERMINATE);
+ }
+ return true;
+ },
+
+ /**
+ * Update the active window's taskbar indicator with the current state. There
+ * are two cases here:
+ * 1. If the active window is the download window, then we always update
+ * the taskbar indicator.
+ * 2. If the active window isn't the download window, then we update only if
+ * the status is normal or indeterminate. i.e. one or more downloads are
+ * currently progressing or in scan mode. If we aren't, then we clear the
+ * indicator.
+ */
+ _updateTaskbar: function DTPU_updateTaskbar()
+ {
+ if (!this._activeTaskbarProgress) {
+ return;
+ }
+
+ if (this._shouldSetState()) {
+ this._activeTaskbarProgress.setProgressState(this._taskbarState,
+ this._totalTransferred,
+ this._totalSize);
+ }
+ // Clear any state otherwise
+ else {
+ this._clearTaskbar();
+ }
+ },
+
+ /**
+ * Clear taskbar state. This is needed:
+ * - to transfer the indicator off a window before transferring it onto
+ * another one
+ * - whenever we don't want to show it for a non-download window.
+ */
+ _clearTaskbar: function DTPU_clearTaskbar()
+ {
+ if (this._activeTaskbarProgress) {
+ this._activeTaskbarProgress.setProgressState(
+ Ci.nsITaskbarProgress.STATE_NO_PROGRESS
+ );
+ }
+ },
+
+ /**
+ * Update this._taskbarState, this._totalSize and this._totalTransferred.
+ * This is called when the download manager is initialized or when the
+ * progress or state of a download changes.
+ * We compute the number of active and paused downloads, and the total size
+ * and total amount already transferred across whichever downloads we have
+ * the data for.
+ * - If there are no active downloads, then we don't want to show any
+ * progress.
+ * - If the number of active downloads is equal to the number of paused
+ * downloads, then we show a paused indicator if we know the size of at
+ * least one download, and no indicator if we don't.
+ * - If the number of active downloads is more than the number of paused
+ * downloads, then we show a "normal" indicator if we know the size of at
+ * least one download, and an indeterminate indicator if we don't.
+ */
+ _updateStatus: function DTPU_updateStatus()
+ {
+ let numActive = this._dm.activeDownloadCount + this._dm.activePrivateDownloadCount;
+ let totalSize = 0, totalTransferred = 0;
+
+ if (numActive == 0) {
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
+ }
+ else {
+ let numPaused = 0, numScanning = 0;
+
+ // Enumerate all active downloads
+ [this._dm.activeDownloads, this._dm.activePrivateDownloads].forEach(function(downloads) {
+ while (downloads.hasMoreElements()) {
+ let download = downloads.getNext().QueryInterface(Ci.nsIDownload);
+ // Only set values if we actually know the download size
+ if (download.percentComplete != -1) {
+ totalSize += download.size;
+ totalTransferred += download.amountTransferred;
+ }
+ // We might need to display a paused state, so track this
+ if (download.state == this._dm.DOWNLOAD_PAUSED) {
+ numPaused++;
+ } else if (download.state == this._dm.DOWNLOAD_SCANNING) {
+ numScanning++;
+ }
+ }
+ }.bind(this));
+
+ // If all downloads are paused, show the progress as paused, unless we
+ // don't have any information about sizes, in which case we don't
+ // display anything
+ if (numActive == numPaused) {
+ if (totalSize == 0) {
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_NO_PROGRESS;
+ totalTransferred = 0;
+ }
+ else {
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_PAUSED;
+ }
+ }
+ // If at least one download is not paused, and we don't have any
+ // information about download sizes, display an indeterminate indicator
+ else if (totalSize == 0 || numActive == numScanning) {
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_INDETERMINATE;
+ totalSize = 0;
+ totalTransferred = 0;
+ }
+ // Otherwise display a normal progress bar
+ else {
+ this._taskbarState = Ci.nsITaskbarProgress.STATE_NORMAL;
+ }
+ }
+
+ this._totalSize = totalSize;
+ this._totalTransferred = totalTransferred;
+ },
+
+ /**
+ * Called when a window that at one point has been an active window is
+ * closed. If this window is currently the active window, we need to look for
+ * another window and make that our active window.
+ *
+ * This function is idempotent, so multiple calls for the same window are not
+ * a problem.
+ *
+ * @param aTaskbarProgress
+ * The taskbar progress for the window that is being unloaded.
+ */
+ _onActiveWindowUnload: function DTPU_onActiveWindowUnload(aTaskbarProgress)
+ {
+ if (this._activeTaskbarProgress == aTaskbarProgress) {
+ let windowMediator = Cc["@mozilla.org/appshell/window-mediator;1"].
+ getService(Ci.nsIWindowMediator);
+ let windows = windowMediator.getEnumerator(null);
+ let newActiveWindow = null;
+ if (windows.hasMoreElements()) {
+ newActiveWindow = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+ }
+
+ // We aren't ever going to reach this point while the download manager is
+ // open, so it's safe to assume false for the second operand
+ this._setActiveWindow(newActiveWindow, false);
+ }
+ },
+
+ // nsIDownloadProgressListener
+
+ /**
+ * Update status if a download's progress has changed.
+ */
+ onProgressChange: function DTPU_onProgressChange()
+ {
+ this._updateStatus();
+ this._updateTaskbar();
+ },
+
+ /**
+ * Update status if a download's state has changed.
+ */
+ onDownloadStateChange: function DTPU_onDownloadStateChange()
+ {
+ this._updateStatus();
+ this._updateTaskbar();
+ },
+
+ onSecurityChange: function() { },
+
+ onStateChange: function() { },
+
+ observe: function DTPU_observe(aSubject, aTopic, aData) {
+ if (aTopic == "quit-application-granted") {
+ this._uninit();
+ }
+ }
+};
diff --git a/toolkit/mozapps/downloads/DownloadUtils.jsm b/toolkit/mozapps/downloads/DownloadUtils.jsm
new file mode 100644
index 000000000..3ebdd605e
--- /dev/null
+++ b/toolkit/mozapps/downloads/DownloadUtils.jsm
@@ -0,0 +1,600 @@
+/* vim: sw=2 ts=2 sts=2 expandtab filetype=javascript
+ * 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 = [ "DownloadUtils" ];
+
+/**
+ * This module provides the DownloadUtils object which contains useful methods
+ * for downloads such as displaying file sizes, transfer times, and download
+ * locations.
+ *
+ * List of methods:
+ *
+ * [string status, double newLast]
+ * getDownloadStatus(int aCurrBytes, [optional] int aMaxBytes,
+ * [optional] double aSpeed, [optional] double aLastSec)
+ *
+ * string progress
+ * getTransferTotal(int aCurrBytes, [optional] int aMaxBytes)
+ *
+ * [string timeLeft, double newLast]
+ * getTimeLeft(double aSeconds, [optional] double aLastSec)
+ *
+ * [string dateCompact, string dateComplete]
+ * getReadableDates(Date aDate, [optional] Date aNow)
+ *
+ * [string displayHost, string fullHost]
+ * getURIHost(string aURIString)
+ *
+ * [string convertedBytes, string units]
+ * convertByteUnits(int aBytes)
+ *
+ * [int time, string units, int subTime, string subUnits]
+ * convertTimeUnits(double aSecs)
+ */
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+this.__defineGetter__("gDecimalSymbol", function() {
+ delete this.gDecimalSymbol;
+ return this.gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
+});
+
+var localeNumberFormatCache = new Map();
+function getLocaleNumberFormat(fractionDigits) {
+ // Backward compatibility: don't use localized digits
+ let locale = Intl.NumberFormat().resolvedOptions().locale +
+ "-u-nu-latn";
+ let key = locale + "_" + fractionDigits;
+ if (!localeNumberFormatCache.has(key)) {
+ localeNumberFormatCache.set(key,
+ Intl.NumberFormat(locale,
+ { maximumFractionDigits: fractionDigits,
+ minimumFractionDigits: fractionDigits }));
+ }
+ return localeNumberFormatCache.get(key);
+}
+
+const kDownloadProperties =
+ "chrome://mozapps/locale/downloads/downloads.properties";
+
+var gStr = {
+ statusFormat: "statusFormat3",
+ statusFormatInfiniteRate: "statusFormatInfiniteRate",
+ statusFormatNoRate: "statusFormatNoRate",
+ transferSameUnits: "transferSameUnits2",
+ transferDiffUnits: "transferDiffUnits2",
+ transferNoTotal: "transferNoTotal2",
+ timePair: "timePair2",
+ timeLeftSingle: "timeLeftSingle2",
+ timeLeftDouble: "timeLeftDouble2",
+ timeFewSeconds: "timeFewSeconds",
+ timeUnknown: "timeUnknown",
+ monthDate: "monthDate2",
+ yesterday: "yesterday",
+ doneScheme: "doneScheme2",
+ doneFileScheme: "doneFileScheme",
+ units: ["bytes", "kilobyte", "megabyte", "gigabyte"],
+ // Update timeSize in convertTimeUnits if changing the length of this array
+ timeUnits: ["seconds", "minutes", "hours", "days"],
+ infiniteRate: "infiniteRate",
+};
+
+// This lazily initializes the string bundle upon first use.
+this.__defineGetter__("gBundle", function() {
+ delete this.gBundle;
+ return this.gBundle = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService).
+ createBundle(kDownloadProperties);
+});
+
+// Keep track of at most this many second/lastSec pairs so that multiple calls
+// to getTimeLeft produce the same time left
+const kCachedLastMaxSize = 10;
+var gCachedLast = [];
+
+this.DownloadUtils = {
+ /**
+ * Generate a full status string for a download given its current progress,
+ * total size, speed, last time remaining
+ *
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @param [optional] aSpeed
+ * Current transfer rate in bytes/sec or -1 for unknown
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A pair: [download status text, new value of "last seconds"]
+ */
+ getDownloadStatus: function DU_getDownloadStatus(aCurrBytes, aMaxBytes,
+ aSpeed, aLastSec)
+ {
+ let [transfer, timeLeft, newLast, normalizedSpeed]
+ = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
+
+ let [rate, unit] = DownloadUtils.convertByteUnits(normalizedSpeed);
+
+ let status;
+ if (rate === "Infinity") {
+ // Infinity download speed doesn't make sense. Show a localized phrase instead.
+ let params = [transfer, gBundle.GetStringFromName(gStr.infiniteRate), timeLeft];
+ status = gBundle.formatStringFromName(gStr.statusFormatInfiniteRate, params,
+ params.length);
+ }
+ else {
+ let params = [transfer, rate, unit, timeLeft];
+ status = gBundle.formatStringFromName(gStr.statusFormat, params,
+ params.length);
+ }
+ return [status, newLast];
+ },
+
+ /**
+ * Generate a status string for a download given its current progress,
+ * total size, speed, last time remaining. The status string contains the
+ * time remaining, as well as the total bytes downloaded. Unlike
+ * getDownloadStatus, it does not include the rate of download.
+ *
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @param [optional] aSpeed
+ * Current transfer rate in bytes/sec or -1 for unknown
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A pair: [download status text, new value of "last seconds"]
+ */
+ getDownloadStatusNoRate:
+ function DU_getDownloadStatusNoRate(aCurrBytes, aMaxBytes, aSpeed,
+ aLastSec)
+ {
+ let [transfer, timeLeft, newLast]
+ = this._deriveTransferRate(aCurrBytes, aMaxBytes, aSpeed, aLastSec);
+
+ let params = [transfer, timeLeft];
+ let status = gBundle.formatStringFromName(gStr.statusFormatNoRate, params,
+ params.length);
+ return [status, newLast];
+ },
+
+ /**
+ * Helper function that returns a transfer string, a time remaining string,
+ * and a new value of "last seconds".
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @param [optional] aSpeed
+ * Current transfer rate in bytes/sec or -1 for unknown
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A triple: [amount transferred string, time remaining string,
+ * new value of "last seconds"]
+ */
+ _deriveTransferRate: function DU__deriveTransferRate(aCurrBytes,
+ aMaxBytes, aSpeed,
+ aLastSec)
+ {
+ if (aMaxBytes == null)
+ aMaxBytes = -1;
+ if (aSpeed == null)
+ aSpeed = -1;
+ if (aLastSec == null)
+ aLastSec = Infinity;
+
+ // Calculate the time remaining if we have valid values
+ let seconds = (aSpeed > 0) && (aMaxBytes > 0) ?
+ (aMaxBytes - aCurrBytes) / aSpeed : -1;
+
+ let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes);
+ let [timeLeft, newLast] = DownloadUtils.getTimeLeft(seconds, aLastSec);
+ return [transfer, timeLeft, newLast, aSpeed];
+ },
+
+ /**
+ * Generate the transfer progress string to show the current and total byte
+ * size. Byte units will be as large as possible and the same units for
+ * current and max will be suppressed for the former.
+ *
+ * @param aCurrBytes
+ * Number of bytes transferred so far
+ * @param [optional] aMaxBytes
+ * Total number of bytes or -1 for unknown
+ * @return The transfer progress text
+ */
+ getTransferTotal: function DU_getTransferTotal(aCurrBytes, aMaxBytes)
+ {
+ if (aMaxBytes == null)
+ aMaxBytes = -1;
+
+ let [progress, progressUnits] = DownloadUtils.convertByteUnits(aCurrBytes);
+ let [total, totalUnits] = DownloadUtils.convertByteUnits(aMaxBytes);
+
+ // Figure out which byte progress string to display
+ let name, values;
+ if (aMaxBytes < 0) {
+ name = gStr.transferNoTotal;
+ values = [
+ progress,
+ progressUnits,
+ ];
+ } else if (progressUnits == totalUnits) {
+ name = gStr.transferSameUnits;
+ values = [
+ progress,
+ total,
+ totalUnits,
+ ];
+ } else {
+ name = gStr.transferDiffUnits;
+ values = [
+ progress,
+ progressUnits,
+ total,
+ totalUnits,
+ ];
+ }
+
+ return gBundle.formatStringFromName(name, values, values.length);
+ },
+
+ /**
+ * Generate a "time left" string given an estimate on the time left and the
+ * last time. The extra time is used to give a better estimate on the time to
+ * show. Both the time values are doubles instead of integers to help get
+ * sub-second accuracy for current and future estimates.
+ *
+ * @param aSeconds
+ * Current estimate on number of seconds left for the download
+ * @param [optional] aLastSec
+ * Last time remaining in seconds or Infinity for unknown
+ * @return A pair: [time left text, new value of "last seconds"]
+ */
+ getTimeLeft: function DU_getTimeLeft(aSeconds, aLastSec)
+ {
+ if (aLastSec == null)
+ aLastSec = Infinity;
+
+ if (aSeconds < 0)
+ return [gBundle.GetStringFromName(gStr.timeUnknown), aLastSec];
+
+ // Try to find a cached lastSec for the given second
+ aLastSec = gCachedLast.reduce((aResult, aItem) =>
+ aItem[0] == aSeconds ? aItem[1] : aResult, aLastSec);
+
+ // Add the current second/lastSec pair unless we have too many
+ gCachedLast.push([aSeconds, aLastSec]);
+ if (gCachedLast.length > kCachedLastMaxSize)
+ gCachedLast.shift();
+
+ // Apply smoothing only if the new time isn't a huge change -- e.g., if the
+ // new time is more than half the previous time; this is useful for
+ // downloads that start/resume slowly
+ if (aSeconds > aLastSec / 2) {
+ // Apply hysteresis to favor downward over upward swings
+ // 30% of down and 10% of up (exponential smoothing)
+ let diff = aSeconds - aLastSec;
+ aSeconds = aLastSec + (diff < 0 ? .3 : .1) * diff;
+
+ // If the new time is similar, reuse something close to the last seconds,
+ // but subtract a little to provide forward progress
+ let diffPct = diff / aLastSec * 100;
+ if (Math.abs(diff) < 5 || Math.abs(diffPct) < 5)
+ aSeconds = aLastSec - (diff < 0 ? .4 : .2);
+ }
+
+ // Decide what text to show for the time
+ let timeLeft;
+ if (aSeconds < 4) {
+ // Be friendly in the last few seconds
+ timeLeft = gBundle.GetStringFromName(gStr.timeFewSeconds);
+ } else {
+ // Convert the seconds into its two largest units to display
+ let [time1, unit1, time2, unit2] =
+ DownloadUtils.convertTimeUnits(aSeconds);
+
+ let pair1 =
+ gBundle.formatStringFromName(gStr.timePair, [time1, unit1], 2);
+ let pair2 =
+ gBundle.formatStringFromName(gStr.timePair, [time2, unit2], 2);
+
+ // Only show minutes for under 1 hour unless there's a few minutes left;
+ // or the second pair is 0.
+ if ((aSeconds < 3600 && time1 >= 4) || time2 == 0) {
+ timeLeft = gBundle.formatStringFromName(gStr.timeLeftSingle,
+ [pair1], 1);
+ } else {
+ // We've got 2 pairs of times to display
+ timeLeft = gBundle.formatStringFromName(gStr.timeLeftDouble,
+ [pair1, pair2], 2);
+ }
+ }
+
+ return [timeLeft, aSeconds];
+ },
+
+ /**
+ * Converts a Date object to two readable formats, one compact, one complete.
+ * The compact format is relative to the current date, and is not an accurate
+ * representation. For example, only the time is displayed for today. The
+ * complete format always includes both the date and the time, excluding the
+ * seconds, and is often shown when hovering the cursor over the compact
+ * representation.
+ *
+ * @param aDate
+ * Date object representing the date and time to format. It is assumed
+ * that this value represents a past date.
+ * @param [optional] aNow
+ * Date object representing the current date and time. The real date
+ * and time of invocation is used if this parameter is omitted.
+ * @return A pair: [compact text, complete text]
+ */
+ getReadableDates: function DU_getReadableDates(aDate, aNow)
+ {
+ if (!aNow) {
+ aNow = new Date();
+ }
+
+ let dts = Cc["@mozilla.org/intl/scriptabledateformat;1"]
+ .getService(Ci.nsIScriptableDateFormat);
+
+ // Figure out when today begins
+ let today = new Date(aNow.getFullYear(), aNow.getMonth(), aNow.getDate());
+
+ // Get locale to use for date/time formatting
+ // TODO: Remove Intl fallback when bug 1215247 is fixed.
+ const locale = typeof Intl === "undefined"
+ ? undefined
+ : Cc["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+
+ // Figure out if the time is from today, yesterday, this week, etc.
+ let dateTimeCompact;
+ if (aDate >= today) {
+ // After today started, show the time
+ dateTimeCompact = dts.FormatTime("",
+ dts.timeFormatNoSeconds,
+ aDate.getHours(),
+ aDate.getMinutes(),
+ 0);
+ } else if (today - aDate < (24 * 60 * 60 * 1000)) {
+ // After yesterday started, show yesterday
+ dateTimeCompact = gBundle.GetStringFromName(gStr.yesterday);
+ } else if (today - aDate < (6 * 24 * 60 * 60 * 1000)) {
+ // After last week started, show day of week
+ dateTimeCompact = typeof Intl === "undefined"
+ ? aDate.toLocaleFormat("%A")
+ : aDate.toLocaleDateString(locale, { weekday: "long" });
+ } else {
+ // Show month/day
+ let month = typeof Intl === "undefined"
+ ? aDate.toLocaleFormat("%B")
+ : aDate.toLocaleDateString(locale, { month: "long" });
+ let date = aDate.getDate();
+ dateTimeCompact = gBundle.formatStringFromName(gStr.monthDate, [month, date], 2);
+ }
+
+ let dateTimeFull = dts.FormatDateTime("",
+ dts.dateFormatLong,
+ dts.timeFormatNoSeconds,
+ aDate.getFullYear(),
+ aDate.getMonth() + 1,
+ aDate.getDate(),
+ aDate.getHours(),
+ aDate.getMinutes(),
+ 0);
+
+ return [dateTimeCompact, dateTimeFull];
+ },
+
+ /**
+ * Get the appropriate display host string for a URI string depending on if
+ * the URI has an eTLD + 1, is an IP address, a local file, or other protocol
+ *
+ * @param aURIString
+ * The URI string to try getting an eTLD + 1, etc.
+ * @return A pair: [display host for the URI string, full host name]
+ */
+ getURIHost: function DU_getURIHost(aURIString)
+ {
+ let ioService = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ let eTLDService = Cc["@mozilla.org/network/effective-tld-service;1"].
+ getService(Ci.nsIEffectiveTLDService);
+ let idnService = Cc["@mozilla.org/network/idn-service;1"].
+ getService(Ci.nsIIDNService);
+
+ // Get a URI that knows about its components
+ let uri;
+ try {
+ uri = ioService.newURI(aURIString, null, null);
+ } catch (ex) {
+ return ["", ""];
+ }
+
+ // Get the inner-most uri for schemes like jar:
+ if (uri instanceof Ci.nsINestedURI)
+ uri = uri.innermostURI;
+
+ let fullHost;
+ try {
+ // Get the full host name; some special URIs fail (data: jar:)
+ fullHost = uri.host;
+ } catch (e) {
+ fullHost = "";
+ }
+
+ let displayHost;
+ try {
+ // This might fail if it's an IP address or doesn't have more than 1 part
+ let baseDomain = eTLDService.getBaseDomain(uri);
+
+ // Convert base domain for display; ignore the isAscii out param
+ displayHost = idnService.convertToDisplayIDN(baseDomain, {});
+ } catch (e) {
+ // Default to the host name
+ displayHost = fullHost;
+ }
+
+ // Check if we need to show something else for the host
+ if (uri.scheme == "file") {
+ // Display special text for file protocol
+ displayHost = gBundle.GetStringFromName(gStr.doneFileScheme);
+ fullHost = displayHost;
+ } else if (displayHost.length == 0) {
+ // Got nothing; show the scheme (data: about: moz-icon:)
+ displayHost =
+ gBundle.formatStringFromName(gStr.doneScheme, [uri.scheme], 1);
+ fullHost = displayHost;
+ } else if (uri.port != -1) {
+ // Tack on the port if it's not the default port
+ let port = ":" + uri.port;
+ displayHost += port;
+ fullHost += port;
+ }
+
+ return [displayHost, fullHost];
+ },
+
+ /**
+ * Converts a number of bytes to the appropriate unit that results in an
+ * internationalized number that needs fewer than 4 digits.
+ *
+ * @param aBytes
+ * Number of bytes to convert
+ * @return A pair: [new value with 3 sig. figs., its unit]
+ */
+ convertByteUnits: function DU_convertByteUnits(aBytes)
+ {
+ let unitIndex = 0;
+
+ // Convert to next unit if it needs 4 digits (after rounding), but only if
+ // we know the name of the next unit
+ while ((aBytes >= 999.5) && (unitIndex < gStr.units.length - 1)) {
+ aBytes /= 1024;
+ unitIndex++;
+ }
+
+ // Get rid of insignificant bits by truncating to 1 or 0 decimal points
+ // 0 -> 0; 1.2 -> 1.2; 12.3 -> 12.3; 123.4 -> 123; 234.5 -> 235
+ // added in bug 462064: (unitIndex != 0) makes sure that no decimal digit for bytes appears when aBytes < 100
+ let fractionDigits = (aBytes > 0) && (aBytes < 100) && (unitIndex != 0) ? 1 : 0;
+
+ // Don't try to format Infinity values using NumberFormat.
+ if (aBytes === Infinity) {
+ aBytes = "Infinity";
+ } else if (typeof Intl != "undefined") {
+ aBytes = getLocaleNumberFormat(fractionDigits)
+ .format(aBytes);
+ } else {
+ // FIXME: Fall back to the old hack, will be fixed in bug 1200494.
+ aBytes = aBytes.toFixed(fractionDigits);
+ if (gDecimalSymbol != ".") {
+ aBytes = aBytes.replace(".", gDecimalSymbol);
+ }
+ }
+
+ return [aBytes, gBundle.GetStringFromName(gStr.units[unitIndex])];
+ },
+
+ /**
+ * Converts a number of seconds to the two largest units. Time values are
+ * whole numbers, and units have the correct plural/singular form.
+ *
+ * @param aSecs
+ * Seconds to convert into the appropriate 2 units
+ * @return 4-item array [first value, its unit, second value, its unit]
+ */
+ convertTimeUnits: function DU_convertTimeUnits(aSecs)
+ {
+ // These are the maximum values for seconds, minutes, hours corresponding
+ // with gStr.timeUnits without the last item
+ let timeSize = [60, 60, 24];
+
+ let time = aSecs;
+ let scale = 1;
+ let unitIndex = 0;
+
+ // Keep converting to the next unit while we have units left and the
+ // current one isn't the largest unit possible
+ while ((unitIndex < timeSize.length) && (time >= timeSize[unitIndex])) {
+ time /= timeSize[unitIndex];
+ scale *= timeSize[unitIndex];
+ unitIndex++;
+ }
+
+ let value = convertTimeUnitsValue(time);
+ let units = convertTimeUnitsUnits(value, unitIndex);
+
+ let extra = aSecs - value * scale;
+ let nextIndex = unitIndex - 1;
+
+ // Convert the extra time to the next largest unit
+ for (let index = 0; index < nextIndex; index++)
+ extra /= timeSize[index];
+
+ let value2 = convertTimeUnitsValue(extra);
+ let units2 = convertTimeUnitsUnits(value2, nextIndex);
+
+ return [value, units, value2, units2];
+ },
+};
+
+/**
+ * Private helper for convertTimeUnits that gets the display value of a time
+ *
+ * @param aTime
+ * Time value for display
+ * @return An integer value for the time rounded down
+ */
+function convertTimeUnitsValue(aTime)
+{
+ return Math.floor(aTime);
+}
+
+/**
+ * Private helper for convertTimeUnits that gets the display units of a time
+ *
+ * @param aTime
+ * Time value for display
+ * @param aIndex
+ * Index into gStr.timeUnits for the appropriate unit
+ * @return The appropriate plural form of the unit for the time
+ */
+function convertTimeUnitsUnits(aTime, aIndex)
+{
+ // Negative index would be an invalid unit, so just give empty
+ if (aIndex < 0)
+ return "";
+
+ return PluralForm.get(aTime, gBundle.GetStringFromName(gStr.timeUnits[aIndex]));
+}
+
+/**
+ * Private helper function to log errors to the error console and command line
+ *
+ * @param aMsg
+ * Error message to log or an array of strings to concat
+ */
+function log(aMsg)
+{
+ let msg = "DownloadUtils.jsm: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
+ logStringMessage(msg);
+ dump(msg + "\n");
+}
diff --git a/toolkit/mozapps/downloads/content/DownloadProgressListener.js b/toolkit/mozapps/downloads/content/DownloadProgressListener.js
new file mode 100644
index 000000000..ab349baf2
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/DownloadProgressListener.js
@@ -0,0 +1,117 @@
+// -*- 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/. */
+
+/**
+ * DownloadProgressListener "class" is used to help update download items shown
+ * in the Download Manager UI such as displaying amount transferred, transfer
+ * rate, and time left for each download.
+ *
+ * This class implements the nsIDownloadProgressListener interface.
+ */
+function DownloadProgressListener() {}
+
+DownloadProgressListener.prototype = {
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDownloadProgressListener]),
+
+ // nsIDownloadProgressListener
+
+ onDownloadStateChange: function dlPL_onDownloadStateChange(aState, aDownload)
+ {
+ // Update window title in-case we don't get all progress notifications
+ onUpdateProgress();
+
+ let dl;
+ let state = aDownload.state;
+ switch (state) {
+ case nsIDM.DOWNLOAD_QUEUED:
+ prependList(aDownload);
+ break;
+
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ prependList(aDownload);
+ // Should fall through, this is a final state but DOWNLOAD_QUEUED
+ // is skipped. See nsDownloadManager::AddDownload.
+ case nsIDM.DOWNLOAD_FAILED:
+ case nsIDM.DOWNLOAD_CANCELED:
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDM.DOWNLOAD_DIRTY:
+ case nsIDM.DOWNLOAD_FINISHED:
+ downloadCompleted(aDownload);
+ if (state == nsIDM.DOWNLOAD_FINISHED)
+ autoRemoveAndClose(aDownload);
+ break;
+ case nsIDM.DOWNLOAD_DOWNLOADING: {
+ dl = getDownload(aDownload.id);
+
+ // At this point, we know if we are an indeterminate download or not
+ dl.setAttribute("progressmode", aDownload.percentComplete == -1 ?
+ "undetermined" : "normal");
+
+ // As well as knowing the referrer
+ let referrer = aDownload.referrer;
+ if (referrer)
+ dl.setAttribute("referrer", referrer.spec);
+
+ break;
+ }
+ }
+
+ // autoRemoveAndClose could have already closed our window...
+ try {
+ if (!dl)
+ dl = getDownload(aDownload.id);
+
+ // Update to the new state
+ dl.setAttribute("state", state);
+
+ // Update ui text values after switching states
+ updateTime(dl);
+ updateStatus(dl);
+ updateButtons(dl);
+ } catch (e) { }
+ },
+
+ onProgressChange: function dlPL_onProgressChange(aWebProgress, aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress, aDownload)
+ {
+ var download = getDownload(aDownload.id);
+
+ // Update this download's progressmeter
+ if (aDownload.percentComplete != -1) {
+ download.setAttribute("progress", aDownload.percentComplete);
+
+ // Dispatch ValueChange for a11y
+ let event = document.createEvent("Events");
+ event.initEvent("ValueChange", true, true);
+ document.getAnonymousElementByAttribute(download, "anonid", "progressmeter")
+ .dispatchEvent(event);
+ }
+
+ // Update the progress so the status can be correctly updated
+ download.setAttribute("currBytes", aDownload.amountTransferred);
+ download.setAttribute("maxBytes", aDownload.size);
+
+ // Update the rest of the UI (bytes transferred, bytes total, download rate,
+ // time remaining).
+ updateStatus(download, aDownload);
+
+ // Update window title
+ onUpdateProgress();
+ },
+
+ onStateChange: function(aWebProgress, aRequest, aState, aStatus, aDownload)
+ {
+ },
+
+ onSecurityChange: function(aWebProgress, aRequest, aState, aDownload)
+ {
+ }
+};
diff --git a/toolkit/mozapps/downloads/content/download.xml b/toolkit/mozapps/downloads/content/download.xml
new file mode 100644
index 000000000..1d4b87270
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/download.xml
@@ -0,0 +1,327 @@
+<?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 bindings [
+ <!ENTITY % downloadDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd" >
+ %downloadDTD;
+]>
+
+<bindings id="downloadBindings"
+ 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">
+
+ <binding id="download-base" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <resources>
+ <stylesheet src="chrome://mozapps/skin/downloads/downloads.css"/>
+ </resources>
+ <implementation>
+ <property name="paused">
+ <getter>
+ <![CDATA[
+ return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_PAUSED;
+ ]]>
+ </getter>
+ </property>
+ <property name="openable">
+ <getter>
+ <![CDATA[
+ return parseInt(this.getAttribute("state")) == Components.interfaces.nsIDownloadManager.DOWNLOAD_FINISHED;
+ ]]>
+ </getter>
+ </property>
+ <property name="inProgress">
+ <getter>
+ <![CDATA[
+ var state = parseInt(this.getAttribute("state"));
+ const dl = Components.interfaces.nsIDownloadManager;
+ return state == dl.DOWNLOAD_NOTSTARTED ||
+ state == dl.DOWNLOAD_QUEUED ||
+ state == dl.DOWNLOAD_DOWNLOADING ||
+ state == dl.DOWNLOAD_PAUSED ||
+ state == dl.DOWNLOAD_SCANNING;
+ ]]>
+ </getter>
+ </property>
+ <property name="removable">
+ <getter>
+ <![CDATA[
+ var state = parseInt(this.getAttribute("state"));
+ const dl = Components.interfaces.nsIDownloadManager;
+ return state == dl.DOWNLOAD_FINISHED ||
+ state == dl.DOWNLOAD_CANCELED ||
+ state == dl.DOWNLOAD_BLOCKED_PARENTAL ||
+ state == dl.DOWNLOAD_BLOCKED_POLICY ||
+ state == dl.DOWNLOAD_DIRTY ||
+ state == dl.DOWNLOAD_FAILED;
+ ]]>
+ </getter>
+ </property>
+ <property name="buttons">
+ <getter>
+ <![CDATA[
+ var startEl = document.getAnonymousNodes(this);
+ if (!startEl.length)
+ startEl = [this];
+
+ const XULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+ return startEl[0].getElementsByTagNameNS(XULNS, "button");
+ ]]>
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+
+ <binding id="download-starting" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" class="name"/>
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"/>
+ <xul:label value="&starting.label;" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ <xul:vbox pack="center">
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-downloading" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1" class="downloadContentBox">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"
+ xbl:inherits="value=progress,mode=progressmode"/>
+ </xul:vbox>
+ <xul:button class="pause mini-button" tooltiptext="&cmd.pause.label;"
+ cmd="cmd_pause" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_pause', this);"/>
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1"
+ crop="right" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-paused" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="normal" value="0" flex="1"
+ anonid="progressmeter"
+ xbl:inherits="value=progress,mode=progressmode"/>
+ </xul:vbox>
+ <xul:button class="resume mini-button" tooltiptext="&cmd.resume.label;"
+ cmd="cmd_resume" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_resume', this);"/>
+ <xul:button class="cancel mini-button" tooltiptext="&cmd.cancel.label;"
+ cmd="cmd_cancel" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_cancel', this);"/>
+ </xul:hbox>
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip" flex="1"
+ crop="right" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-done" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-canceled" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;"
+ cmd="cmd_retry" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_retry', this);"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-failed" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ <xul:button class="retry mini-button" tooltiptext="&cmd.retry.label;"
+ cmd="cmd_retry" ondblclick="event.stopPropagation();"
+ oncommand="performCommand('cmd_retry', this);"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-blocked-parental" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-blocked-policy" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-scanning" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon" validate="always"
+ xbl:inherits="src=image"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="2" class="name"/>
+ <xul:hbox>
+ <xul:vbox flex="1">
+ <xul:progressmeter mode="undetermined" flex="1" />
+ </xul:vbox>
+ </xul:hbox>
+ <xul:label value="&scanning.label;" class="status"/>
+ <xul:spacer flex="1"/>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="download-dirty" extends="chrome://mozapps/content/downloads/download.xml#download-base">
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox pack="center">
+ <xul:image class="downloadTypeIcon blockedIcon"/>
+ </xul:vbox>
+ <xul:vbox pack="start" flex="1">
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=target,tooltiptext=target"
+ crop="center" flex="1" class="name"/>
+ <xul:label xbl:inherits="value=dateTime,tooltiptext=dateTimeTip"
+ class="dateTime"/>
+ </xul:hbox>
+ <xul:hbox align="center" flex="1">
+ <xul:label xbl:inherits="value=status,tooltiptext=statusTip"
+ crop="end" flex="1" class="status"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+</bindings>
diff --git a/toolkit/mozapps/downloads/content/downloads.css b/toolkit/mozapps/downloads/content/downloads.css
new file mode 100644
index 000000000..dcb648d62
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/downloads.css
@@ -0,0 +1,50 @@
+/* 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[type="download"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-starting');
+ -moz-box-orient: vertical;
+}
+
+richlistitem[type="download"][state="0"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-downloading');
+}
+
+richlistitem[type="download"][state="1"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-done');
+}
+
+richlistitem[type="download"][state="2"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-failed');
+}
+
+richlistitem[type="download"][state="3"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-canceled');
+}
+
+richlistitem[type="download"][state="4"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-paused');
+}
+
+richlistitem[type="download"][state="6"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-parental');
+}
+
+richlistitem[type="download"][state="7"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-scanning');
+}
+
+richlistitem[type="download"][state="8"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-dirty');
+}
+
+richlistitem[type="download"][state="9"] {
+ -moz-binding: url('chrome://mozapps/content/downloads/download.xml#download-blocked-policy');
+}
+
+/* Only focus buttons in the selected item*/
+richlistitem[type="download"]:not([selected="true"]) button {
+ -moz-user-focus: none;
+}
+
diff --git a/toolkit/mozapps/downloads/content/downloads.js b/toolkit/mozapps/downloads/content/downloads.js
new file mode 100644
index 000000000..92a9f7593
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/downloads.js
@@ -0,0 +1,1320 @@
+/* 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";
+
+// Globals
+
+const PREF_BDM_CLOSEWHENDONE = "browser.download.manager.closeWhenDone";
+const PREF_BDM_ALERTONEXEOPEN = "browser.download.manager.alertOnEXEOpen";
+const PREF_BDM_SCANWHENDONE = "browser.download.manager.scanWhenDone";
+
+const nsLocalFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "PluralForm",
+ "resource://gre/modules/PluralForm.jsm");
+
+const nsIDM = Ci.nsIDownloadManager;
+
+var gDownloadManager = Cc["@mozilla.org/download-manager;1"].getService(nsIDM);
+var gDownloadManagerUI = Cc["@mozilla.org/download-manager-ui;1"].
+ getService(Ci.nsIDownloadManagerUI);
+
+var gDownloadListener = null;
+var gDownloadsView = null;
+var gSearchBox = null;
+var gSearchTerms = [];
+var gBuilder = 0;
+
+// This variable is used when performing commands on download items and gives
+// the command the ability to do something after all items have been operated
+// on. The following convention is used to handle the value of the variable:
+// whenever we aren't performing a command, the value is |undefined|; just
+// before executing commands, the value will be set to |null|; and when
+// commands want to create a callback, they set the value to be a callback
+// function to be executed after all download items have been visited.
+var gPerformAllCallback;
+
+// Control the performance of the incremental list building by setting how many
+// milliseconds to wait before building more of the list and how many items to
+// add between each delay.
+const gListBuildDelay = 300;
+const gListBuildChunk = 3;
+
+// Array of download richlistitem attributes to check when searching
+const gSearchAttributes = [
+ "target",
+ "status",
+ "dateTime",
+];
+
+// If the user has interacted with the window in a significant way, we should
+// not auto-close the window. Tough UI decisions about what is "significant."
+var gUserInteracted = false;
+
+// These strings will be converted to the corresponding ones from the string
+// bundle on startup.
+var gStr = {
+ paused: "paused",
+ cannotPause: "cannotPause",
+ doneStatus: "doneStatus",
+ doneSize: "doneSize",
+ doneSizeUnknown: "doneSizeUnknown",
+ stateFailed: "stateFailed",
+ stateCanceled: "stateCanceled",
+ stateBlockedParentalControls: "stateBlocked",
+ stateBlockedPolicy: "stateBlockedPolicy",
+ stateDirty: "stateDirty",
+ downloadsTitleFiles: "downloadsTitleFiles",
+ downloadsTitlePercent: "downloadsTitlePercent",
+ fileExecutableSecurityWarningTitle: "fileExecutableSecurityWarningTitle",
+ fileExecutableSecurityWarningDontAsk: "fileExecutableSecurityWarningDontAsk"
+};
+
+// The statement to query for downloads that are active or match the search
+var gStmt = null;
+
+// Start/Stop Observers
+
+function downloadCompleted(aDownload)
+{
+ // The download is changing state, so update the clear list button
+ updateClearListButton();
+
+ // Wrap this in try...catch since this can be called while shutting down...
+ // it doesn't really matter if it fails then since well.. we're shutting down
+ // and there's no UI to update!
+ try {
+ let dl = getDownload(aDownload.id);
+
+ // Update attributes now that we've finished
+ dl.setAttribute("startTime", Math.round(aDownload.startTime / 1000));
+ dl.setAttribute("endTime", Date.now());
+ dl.setAttribute("currBytes", aDownload.amountTransferred);
+ dl.setAttribute("maxBytes", aDownload.size);
+
+ // Move the download below active if it should stay in the list
+ if (downloadMatchesSearch(dl)) {
+ // Iterate down until we find a non-active download
+ let next = dl.nextSibling;
+ while (next && next.inProgress)
+ next = next.nextSibling;
+
+ // Move the item
+ gDownloadsView.insertBefore(dl, next);
+ } else {
+ removeFromView(dl);
+ }
+
+ // getTypeFromFile fails if it can't find a type for this file.
+ try {
+ // Refresh the icon, so that executable icons are shown.
+ var mimeService = Cc["@mozilla.org/mime;1"].
+ getService(Ci.nsIMIMEService);
+ var contentType = mimeService.getTypeFromFile(aDownload.targetFile);
+
+ var listItem = getDownload(aDownload.id)
+ var oldImage = listItem.getAttribute("image");
+ // Tacking on contentType bypasses cache
+ listItem.setAttribute("image", oldImage + "&contentType=" + contentType);
+ } catch (e) { }
+
+ if (gDownloadManager.activeDownloadCount == 0)
+ document.title = document.documentElement.getAttribute("statictitle");
+
+ gDownloadManagerUI.getAttention();
+ }
+ catch (e) { }
+}
+
+function autoRemoveAndClose(aDownload)
+{
+ var pref = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+
+ if (gDownloadManager.activeDownloadCount == 0) {
+ // For the moment, just use the simple heuristic that if this window was
+ // opened by the download process, rather than by the user, it should
+ // auto-close if the pref is set that way. If the user opened it themselves,
+ // it should not close until they explicitly close it. Additionally, the
+ // preference to control the feature may not be set, so defaulting to
+ // keeping the window open.
+ let autoClose = false;
+ try {
+ autoClose = pref.getBoolPref(PREF_BDM_CLOSEWHENDONE);
+ } catch (e) { }
+ var autoOpened =
+ !window.opener || window.opener.location.href == window.location.href;
+ if (autoClose && autoOpened && !gUserInteracted) {
+ gCloseDownloadManager();
+ return true;
+ }
+ }
+
+ return false;
+}
+
+// This function can be overwritten by extensions that wish to place the
+// Download Window in another part of the UI.
+function gCloseDownloadManager()
+{
+ window.close();
+}
+
+// Download Event Handlers
+
+function cancelDownload(aDownload)
+{
+ gDownloadManager.cancelDownload(aDownload.getAttribute("dlid"));
+
+ // XXXben -
+ // If we got here because we resumed the download, we weren't using a temp file
+ // because we used saveURL instead. (this is because the proper download mechanism
+ // employed by the helper app service isn't fully accessible yet... should be fixed...
+ // talk to bz...)
+ // the upshot is we have to delete the file if it exists.
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+
+ if (f.exists())
+ f.remove(false);
+}
+
+function pauseDownload(aDownload)
+{
+ var id = aDownload.getAttribute("dlid");
+ gDownloadManager.pauseDownload(id);
+}
+
+function resumeDownload(aDownload)
+{
+ gDownloadManager.resumeDownload(aDownload.getAttribute("dlid"));
+}
+
+function removeDownload(aDownload)
+{
+ gDownloadManager.removeDownload(aDownload.getAttribute("dlid"));
+}
+
+function retryDownload(aDownload)
+{
+ removeFromView(aDownload);
+ gDownloadManager.retryDownload(aDownload.getAttribute("dlid"));
+}
+
+function showDownload(aDownload)
+{
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+
+ try {
+ // Show the directory containing the file and select the file
+ f.reveal();
+ } catch (e) {
+ // If reveal fails for some reason (e.g., it's not implemented on unix or
+ // the file doesn't exist), try using the parent if we have it.
+ let parent = f.parent.QueryInterface(Ci.nsILocalFile);
+ if (!parent)
+ return;
+
+ try {
+ // "Double click" the parent directory to show where the file should be
+ parent.launch();
+ } catch (e) {
+ // If launch also fails (probably because it's not implemented), let the
+ // OS handler try to open the parent
+ openExternal(parent);
+ }
+ }
+}
+
+function onDownloadDblClick(aEvent)
+{
+ // Only do the default action for double primary clicks
+ if (aEvent.button == 0 && aEvent.target.selected)
+ doDefaultForSelected();
+}
+
+function openDownload(aDownload)
+{
+ var f = getLocalFileFromNativePathOrUrl(aDownload.getAttribute("file"));
+ if (f.isExecutable()) {
+ var dontAsk = false;
+ var pref = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ try {
+ dontAsk = !pref.getBoolPref(PREF_BDM_ALERTONEXEOPEN);
+ } catch (e) { }
+
+ if (AppConstants.platform == "win") {
+ // On Vista and above, we rely on native security prompting for
+ // downloaded content unless it's disabled.
+ try {
+ var sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ if (parseFloat(sysInfo.getProperty("version")) >= 6 &&
+ pref.getBoolPref(PREF_BDM_SCANWHENDONE)) {
+ dontAsk = true;
+ }
+ } catch (ex) { }
+ }
+
+ if (!dontAsk) {
+ var strings = document.getElementById("downloadStrings");
+ var name = aDownload.getAttribute("target");
+ var message = strings.getFormattedString("fileExecutableSecurityWarning", [name, name]);
+
+ let title = gStr.fileExecutableSecurityWarningTitle;
+ let dontAsk = gStr.fileExecutableSecurityWarningDontAsk;
+
+ var promptSvc = Cc["@mozilla.org/embedcomp/prompt-service;1"].
+ getService(Ci.nsIPromptService);
+ var checkbox = { value: false };
+ var open = promptSvc.confirmCheck(window, title, message, dontAsk, checkbox);
+
+ if (!open)
+ return;
+ pref.setBoolPref(PREF_BDM_ALERTONEXEOPEN, !checkbox.value);
+ }
+ }
+ try {
+ try {
+ let download = gDownloadManager.getDownload(aDownload.getAttribute("dlid"));
+ let mimeInfo = download.MIMEInfo;
+ if (mimeInfo.preferredAction == mimeInfo.useHelperApp) {
+ mimeInfo.launchWithFile(f);
+ return;
+ }
+ } catch (ex) {
+ }
+ f.launch();
+ } catch (ex) {
+ // if launch fails, try sending it through the system's external
+ // file: URL handler
+ openExternal(f);
+ }
+}
+
+function openReferrer(aDownload)
+{
+ openURL(getReferrerOrSource(aDownload));
+}
+
+function copySourceLocation(aDownload)
+{
+ var uri = aDownload.getAttribute("uri");
+ var clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].
+ getService(Ci.nsIClipboardHelper);
+
+ // Check if we should initialize a callback
+ if (gPerformAllCallback === null) {
+ let uris = [];
+ gPerformAllCallback = aURI => aURI ? uris.push(aURI) :
+ clipboard.copyString(uris.join("\n"));
+ }
+
+ // We have a callback to use, so use it to add a uri
+ if (typeof gPerformAllCallback == "function")
+ gPerformAllCallback(uri);
+ else {
+ // It's a plain copy source, so copy it
+ clipboard.copyString(uri);
+ }
+}
+
+/**
+ * Remove the currently shown downloads from the download list.
+ */
+function clearDownloadList() {
+ // Clear the whole list if there's no search
+ if (gSearchTerms == "") {
+ gDownloadManager.cleanUp();
+ return;
+ }
+
+ // Remove each download starting from the end until we hit a download
+ // that is in progress
+ let item;
+ while ((item = gDownloadsView.lastChild) && !item.inProgress)
+ removeDownload(item);
+
+ // Clear the input as if the user did it and move focus to the list
+ gSearchBox.value = "";
+ gSearchBox.doCommand();
+ gDownloadsView.focus();
+}
+
+// This is called by the progress listener.
+var gLastComputedMean = -1;
+var gLastActiveDownloads = 0;
+function onUpdateProgress()
+{
+ let numActiveDownloads = gDownloadManager.activeDownloadCount;
+
+ // Use the default title and reset "last" values if there's no downloads
+ if (numActiveDownloads == 0) {
+ document.title = document.documentElement.getAttribute("statictitle");
+ gLastComputedMean = -1;
+ gLastActiveDownloads = 0;
+
+ return;
+ }
+
+ // Establish the mean transfer speed and amount downloaded.
+ var mean = 0;
+ var base = 0;
+ var dls = gDownloadManager.activeDownloads;
+ while (dls.hasMoreElements()) {
+ let dl = dls.getNext();
+ if (dl.percentComplete < 100 && dl.size > 0) {
+ mean += dl.amountTransferred;
+ base += dl.size;
+ }
+ }
+
+ // Calculate the percent transferred, unless we don't have a total file size
+ let title = gStr.downloadsTitlePercent;
+ if (base == 0)
+ title = gStr.downloadsTitleFiles;
+ else
+ mean = Math.floor((mean / base) * 100);
+
+ // Update title of window
+ if (mean != gLastComputedMean || gLastActiveDownloads != numActiveDownloads) {
+ gLastComputedMean = mean;
+ gLastActiveDownloads = numActiveDownloads;
+
+ // Get the correct plural form and insert number of downloads and percent
+ title = PluralForm.get(numActiveDownloads, title);
+ title = replaceInsert(title, 1, numActiveDownloads);
+ title = replaceInsert(title, 2, mean);
+
+ document.title = title;
+ }
+}
+
+// Startup, Shutdown
+
+function Startup()
+{
+ gDownloadsView = document.getElementById("downloadView");
+ gSearchBox = document.getElementById("searchbox");
+
+ // convert strings to those in the string bundle
+ let sb = document.getElementById("downloadStrings");
+ let getStr = string => sb.getString(string);
+ for (let [name, value] of Object.entries(gStr))
+ gStr[name] = typeof value == "string" ? getStr(value) : value.map(getStr);
+
+ initStatement();
+ buildDownloadList(true);
+
+ // The DownloadProgressListener (DownloadProgressListener.js) handles progress
+ // notifications.
+ gDownloadListener = new DownloadProgressListener();
+ gDownloadManager.addListener(gDownloadListener);
+
+ // If the UI was displayed because the user interacted, we need to make sure
+ // we update gUserInteracted accordingly.
+ if (window.arguments[1] == Ci.nsIDownloadManagerUI.REASON_USER_INTERACTED)
+ gUserInteracted = true;
+
+ // downloads can finish before Startup() does, so check if the window should
+ // close and act accordingly
+ if (!autoRemoveAndClose())
+ gDownloadsView.focus();
+
+ let obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ obs.addObserver(gDownloadObserver, "download-manager-remove-download", false);
+ obs.addObserver(gDownloadObserver, "browser-lastwindow-close-granted", false);
+
+ // Clear the search box and move focus to the list on escape from the box
+ gSearchBox.addEventListener("keypress", function(e) {
+ if (e.keyCode == e.DOM_VK_ESCAPE) {
+ // Move focus to the list instead of closing the window
+ gDownloadsView.focus();
+ e.preventDefault();
+ }
+ }, false);
+
+ let DownloadTaskbarProgress =
+ Cu.import("resource://gre/modules/DownloadTaskbarProgress.jsm", {}).DownloadTaskbarProgress;
+ DownloadTaskbarProgress.onDownloadWindowLoad(window);
+}
+
+function Shutdown()
+{
+ gDownloadManager.removeListener(gDownloadListener);
+
+ let obs = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ obs.removeObserver(gDownloadObserver, "download-manager-remove-download");
+ obs.removeObserver(gDownloadObserver, "browser-lastwindow-close-granted");
+
+ clearTimeout(gBuilder);
+ gStmt.reset();
+ gStmt.finalize();
+}
+
+var gDownloadObserver = {
+ observe: function gdo_observe(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "download-manager-remove-download":
+ // A null subject here indicates "remove multiple", so we just rebuild.
+ if (!aSubject) {
+ // Rebuild the default view
+ buildDownloadList(true);
+ break;
+ }
+
+ // Otherwise, remove a single download
+ let id = aSubject.QueryInterface(Ci.nsISupportsPRUint32);
+ let dl = getDownload(id.data);
+ removeFromView(dl);
+ break;
+ case "browser-lastwindow-close-granted":
+ if (AppConstants.platform != "macosx" &&
+ gDownloadManager.activeDownloadCount == 0) {
+ setTimeout(gCloseDownloadManager, 0);
+ }
+ break;
+ }
+ }
+};
+
+// View Context Menus
+
+var gContextMenus = [
+ // DOWNLOAD_DOWNLOADING
+ [
+ "menuitem_pause"
+ , "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_FINISHED
+ [
+ "menuitem_open"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_FAILED
+ [
+ "menuitem_retry"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_CANCELED
+ [
+ "menuitem_retry"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_PAUSED
+ [
+ "menuitem_resume"
+ , "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_QUEUED
+ [
+ "menuitem_cancel"
+ , "menuseparator"
+ , "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_BLOCKED_PARENTAL
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_SCANNING
+ [
+ "menuitem_show"
+ , "menuseparator"
+ , "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ ],
+ // DOWNLOAD_DIRTY
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ],
+ // DOWNLOAD_BLOCKED_POLICY
+ [
+ "menuitem_openReferrer"
+ , "menuitem_copyLocation"
+ , "menuseparator"
+ , "menuitem_selectAll"
+ , "menuseparator"
+ , "menuitem_removeFromList"
+ ]
+];
+
+function buildContextMenu(aEvent)
+{
+ if (aEvent.target.id != "downloadContextMenu")
+ return false;
+
+ var popup = document.getElementById("downloadContextMenu");
+ while (popup.hasChildNodes())
+ popup.removeChild(popup.firstChild);
+
+ if (gDownloadsView.selectedItem) {
+ let dl = gDownloadsView.selectedItem;
+ let idx = parseInt(dl.getAttribute("state"));
+ if (idx < 0)
+ idx = 0;
+
+ var menus = gContextMenus[idx];
+ for (let i = 0; i < menus.length; ++i) {
+ let menuitem = document.getElementById(menus[i]).cloneNode(true);
+ let cmd = menuitem.getAttribute("cmd");
+ if (cmd)
+ menuitem.disabled = !gDownloadViewController.isCommandEnabled(cmd, dl);
+
+ popup.appendChild(menuitem);
+ }
+
+ return true;
+ }
+
+ return false;
+}
+// Drag and Drop
+var gDownloadDNDObserver =
+{
+ onDragStart: function (aEvent)
+ {
+ if (!gDownloadsView.selectedItem)
+ return;
+ var dl = gDownloadsView.selectedItem;
+ var f = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ if (!f.exists())
+ return;
+
+ var dt = aEvent.dataTransfer;
+ dt.mozSetDataAt("application/x-moz-file", f, 0);
+ var url = Services.io.newFileURI(f).spec;
+ dt.setData("text/uri-list", url);
+ dt.setData("text/plain", url);
+ dt.effectAllowed = "copyMove";
+ dt.addElement(dl);
+ },
+
+ onDragOver: function (aEvent)
+ {
+ var types = aEvent.dataTransfer.types;
+ if (types.includes("text/uri-list") ||
+ types.includes("text/x-moz-url") ||
+ types.includes("text/plain"))
+ aEvent.preventDefault();
+ },
+
+ onDrop: function(aEvent)
+ {
+ var dt = aEvent.dataTransfer;
+ // If dragged item is from our source, do not try to
+ // redownload already downloaded file.
+ if (dt.mozGetDataAt("application/x-moz-file", 0))
+ return;
+
+ var url = dt.getData("URL");
+ var name;
+ if (!url) {
+ url = dt.getData("text/x-moz-url") || dt.getData("text/plain");
+ [url, name] = url.split("\n");
+ }
+ if (url) {
+ let sourceDoc = dt.mozSourceNode ? dt.mozSourceNode.ownerDocument : document;
+ saveURL(url, name ? name : url, null, true, true, null, sourceDoc);
+ }
+ }
+}
+
+function pasteHandler() {
+ let trans = Cc["@mozilla.org/widget/transferable;1"].
+ createInstance(Ci.nsITransferable);
+ trans.init(null);
+ let flavors = ["text/x-moz-url", "text/unicode"];
+ flavors.forEach(trans.addDataFlavor);
+
+ Services.clipboard.getData(trans, Services.clipboard.kGlobalClipboard);
+
+ // Getting the data or creating the nsIURI might fail
+ try {
+ let data = {};
+ trans.getAnyTransferData({}, data, {});
+ let [url, name] = data.value.QueryInterface(Ci.nsISupportsString).data.split("\n");
+
+ if (!url)
+ return;
+
+ let uri = Services.io.newURI(url, null, null);
+
+ saveURL(uri.spec, name || uri.spec, null, true, true, null, document);
+ } catch (ex) {}
+}
+
+// Command Updating and Command Handlers
+
+var gDownloadViewController = {
+ isCommandEnabled: function(aCommand, aItem)
+ {
+ let dl = aItem;
+ let download = null; // used for getting an nsIDownload object
+
+ switch (aCommand) {
+ case "cmd_cancel":
+ return dl.inProgress;
+ case "cmd_open": {
+ let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ return dl.openable && file.exists();
+ }
+ case "cmd_show": {
+ let file = getLocalFileFromNativePathOrUrl(dl.getAttribute("file"));
+ return file.exists();
+ }
+ case "cmd_pause":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return dl.inProgress && !dl.paused && download.resumable;
+ case "cmd_pauseResume":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return (dl.inProgress || dl.paused) && download.resumable;
+ case "cmd_resume":
+ download = gDownloadManager.getDownload(dl.getAttribute("dlid"));
+ return dl.paused && download.resumable;
+ case "cmd_openReferrer":
+ return dl.hasAttribute("referrer");
+ case "cmd_removeFromList":
+ case "cmd_retry":
+ return dl.removable;
+ case "cmd_copyLocation":
+ return true;
+ }
+ return false;
+ },
+
+ doCommand: function(aCommand, aItem)
+ {
+ if (this.isCommandEnabled(aCommand, aItem))
+ this.commands[aCommand](aItem);
+ },
+
+ commands: {
+ cmd_cancel: function(aSelectedItem) {
+ cancelDownload(aSelectedItem);
+ },
+ cmd_open: function(aSelectedItem) {
+ openDownload(aSelectedItem);
+ },
+ cmd_openReferrer: function(aSelectedItem) {
+ openReferrer(aSelectedItem);
+ },
+ cmd_pause: function(aSelectedItem) {
+ pauseDownload(aSelectedItem);
+ },
+ cmd_pauseResume: function(aSelectedItem) {
+ if (aSelectedItem.paused)
+ this.cmd_resume(aSelectedItem);
+ else
+ this.cmd_pause(aSelectedItem);
+ },
+ cmd_removeFromList: function(aSelectedItem) {
+ removeDownload(aSelectedItem);
+ },
+ cmd_resume: function(aSelectedItem) {
+ resumeDownload(aSelectedItem);
+ },
+ cmd_retry: function(aSelectedItem) {
+ retryDownload(aSelectedItem);
+ },
+ cmd_show: function(aSelectedItem) {
+ showDownload(aSelectedItem);
+ },
+ cmd_copyLocation: function(aSelectedItem) {
+ copySourceLocation(aSelectedItem);
+ },
+ }
+};
+
+/**
+ * Helper function to do commands.
+ *
+ * @param aCmd
+ * The command to be performed.
+ * @param aItem
+ * The richlistitem that represents the download that will have the
+ * command performed on it. If this is null, the command is performed on
+ * all downloads. If the item passed in is not a richlistitem that
+ * represents a download, it will walk up the parent nodes until it finds
+ * a DOM node that is.
+ */
+function performCommand(aCmd, aItem)
+{
+ let elm = aItem;
+ if (!elm) {
+ // If we don't have a desired download item, do the command for all
+ // selected items. Initialize the callback to null so commands know to add
+ // a callback if they want. We will call the callback with empty arguments
+ // after performing the command on each selected download item.
+ gPerformAllCallback = null;
+
+ // Convert the nodelist into an array to keep a copy of the download items
+ let items = [];
+ for (let i = gDownloadsView.selectedItems.length; --i >= 0; )
+ items.unshift(gDownloadsView.selectedItems[i]);
+
+ // Do the command for each download item
+ for (let item of items)
+ performCommand(aCmd, item);
+
+ // Call the callback with no arguments and reset because we're done
+ if (typeof gPerformAllCallback == "function")
+ gPerformAllCallback();
+ gPerformAllCallback = undefined;
+
+ return;
+ }
+ while (elm.nodeName != "richlistitem" ||
+ elm.getAttribute("type") != "download") {
+ elm = elm.parentNode;
+ }
+
+ gDownloadViewController.doCommand(aCmd, elm);
+}
+
+function setSearchboxFocus()
+{
+ gSearchBox.focus();
+ gSearchBox.select();
+}
+
+function openExternal(aFile)
+{
+ var uri = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService).newFileURI(aFile);
+
+ var protocolSvc = Cc["@mozilla.org/uriloader/external-protocol-service;1"].
+ getService(Ci.nsIExternalProtocolService);
+ protocolSvc.loadUrl(uri);
+
+ return;
+}
+
+// Utility Functions
+
+/**
+ * Create a download richlistitem with the provided attributes. Some attributes
+ * are *required* while optional ones will only be set on the item if provided.
+ *
+ * @param aAttrs
+ * An object that must have the following properties: dlid, file,
+ * target, uri, state, progress, startTime, endTime, currBytes,
+ * maxBytes; optional properties: referrer
+ * @return An initialized download richlistitem
+ */
+function createDownloadItem(aAttrs)
+{
+ let dl = document.createElement("richlistitem");
+
+ // Copy the attributes from the argument into the item
+ for (let attr in aAttrs)
+ dl.setAttribute(attr, aAttrs[attr]);
+
+ // Initialize other attributes
+ dl.setAttribute("type", "download");
+ dl.setAttribute("id", "dl" + aAttrs.dlid);
+ dl.setAttribute("image", "moz-icon://" + aAttrs.file + "?size=32");
+ dl.setAttribute("lastSeconds", Infinity);
+
+ // Initialize more complex attributes
+ updateTime(dl);
+ updateStatus(dl);
+
+ try {
+ let file = getLocalFileFromNativePathOrUrl(aAttrs.file);
+ dl.setAttribute("path", file.nativePath || file.path);
+ return dl;
+ } catch (e) {
+ // aFile might not be a file: url or a valid native path
+ // see bug #392386 for details
+ }
+ return null;
+}
+
+/**
+ * Updates the disabled state of the buttons of a downlaod.
+ *
+ * @param aItem
+ * The richlistitem representing the download.
+ */
+function updateButtons(aItem)
+{
+ let buttons = aItem.buttons;
+
+ for (let i = 0; i < buttons.length; ++i) {
+ let cmd = buttons[i].getAttribute("cmd");
+ let enabled = gDownloadViewController.isCommandEnabled(cmd, aItem);
+ buttons[i].disabled = !enabled;
+
+ if ("cmd_pause" == cmd && !enabled) {
+ // We need to add the tooltip indicating that the download cannot be
+ // paused now.
+ buttons[i].setAttribute("tooltiptext", gStr.cannotPause);
+ }
+ }
+}
+
+/**
+ * Updates the status for a download item depending on its state
+ *
+ * @param aItem
+ * The richlistitem that has various download attributes.
+ * @param aDownload
+ * The nsDownload from the backend. This is an optional parameter, but
+ * is useful for certain states such as DOWNLOADING.
+ */
+function updateStatus(aItem, aDownload) {
+ let status = "";
+ let statusTip = "";
+
+ let state = Number(aItem.getAttribute("state"));
+ switch (state) {
+ case nsIDM.DOWNLOAD_PAUSED:
+ {
+ let currBytes = Number(aItem.getAttribute("currBytes"));
+ let maxBytes = Number(aItem.getAttribute("maxBytes"));
+
+ let transfer = DownloadUtils.getTransferTotal(currBytes, maxBytes);
+ status = replaceInsert(gStr.paused, 1, transfer);
+
+ break;
+ }
+ case nsIDM.DOWNLOAD_DOWNLOADING:
+ {
+ let currBytes = Number(aItem.getAttribute("currBytes"));
+ let maxBytes = Number(aItem.getAttribute("maxBytes"));
+ // If we don't have an active download, assume 0 bytes/sec
+ let speed = aDownload ? aDownload.speed : 0;
+ let lastSec = Number(aItem.getAttribute("lastSeconds"));
+
+ let newLast;
+ [status, newLast] =
+ DownloadUtils.getDownloadStatus(currBytes, maxBytes, speed, lastSec);
+
+ // Update lastSeconds to be the new value
+ aItem.setAttribute("lastSeconds", newLast);
+
+ break;
+ }
+ case nsIDM.DOWNLOAD_FINISHED:
+ case nsIDM.DOWNLOAD_FAILED:
+ case nsIDM.DOWNLOAD_CANCELED:
+ case nsIDM.DOWNLOAD_BLOCKED_PARENTAL:
+ case nsIDM.DOWNLOAD_BLOCKED_POLICY:
+ case nsIDM.DOWNLOAD_DIRTY:
+ {
+ let stateSize = {};
+ stateSize[nsIDM.DOWNLOAD_FINISHED] = function() {
+ // Display the file size, but show "Unknown" for negative sizes
+ let fileSize = Number(aItem.getAttribute("maxBytes"));
+ let sizeText = gStr.doneSizeUnknown;
+ if (fileSize >= 0) {
+ let [size, unit] = DownloadUtils.convertByteUnits(fileSize);
+ sizeText = replaceInsert(gStr.doneSize, 1, size);
+ sizeText = replaceInsert(sizeText, 2, unit);
+ }
+ return sizeText;
+ };
+ stateSize[nsIDM.DOWNLOAD_FAILED] = () => gStr.stateFailed;
+ stateSize[nsIDM.DOWNLOAD_CANCELED] = () => gStr.stateCanceled;
+ stateSize[nsIDM.DOWNLOAD_BLOCKED_PARENTAL] = () => gStr.stateBlockedParentalControls;
+ stateSize[nsIDM.DOWNLOAD_BLOCKED_POLICY] = () => gStr.stateBlockedPolicy;
+ stateSize[nsIDM.DOWNLOAD_DIRTY] = () => gStr.stateDirty;
+
+ // Insert 1 is the download size or download state
+ status = replaceInsert(gStr.doneStatus, 1, stateSize[state]());
+
+ let [displayHost, fullHost] =
+ DownloadUtils.getURIHost(getReferrerOrSource(aItem));
+
+ // Insert 2 is the eTLD + 1 or other variations of the host
+ status = replaceInsert(status, 2, displayHost);
+ // Set the tooltip to be the full host
+ statusTip = fullHost;
+
+ break;
+ }
+ }
+
+ aItem.setAttribute("status", status);
+ aItem.setAttribute("statusTip", statusTip != "" ? statusTip : status);
+}
+
+/**
+ * Updates the time that gets shown for completed download items
+ *
+ * @param aItem
+ * The richlistitem representing a download in the UI
+ */
+function updateTime(aItem)
+{
+ // Don't bother updating for things that aren't finished
+ if (aItem.inProgress)
+ return;
+
+ let end = new Date(parseInt(aItem.getAttribute("endTime")));
+ let [dateCompact, dateComplete] = DownloadUtils.getReadableDates(end);
+ aItem.setAttribute("dateTime", dateCompact);
+ aItem.setAttribute("dateTimeTip", dateComplete);
+}
+
+/**
+ * Helper function to replace a placeholder string with a real string
+ *
+ * @param aText
+ * Source text containing placeholder (e.g., #1)
+ * @param aIndex
+ * Index number of placeholder to replace
+ * @param aValue
+ * New string to put in place of placeholder
+ * @return The string with placeholder replaced with the new string
+ */
+function replaceInsert(aText, aIndex, aValue)
+{
+ return aText.replace("#" + aIndex, aValue);
+}
+
+/**
+ * Perform the default action for the currently selected download item
+ */
+function doDefaultForSelected()
+{
+ // Make sure we have something selected
+ let item = gDownloadsView.selectedItem;
+ if (!item)
+ return;
+
+ // Get the default action (first item in the menu)
+ let state = Number(item.getAttribute("state"));
+ let menuitem = document.getElementById(gContextMenus[state][0]);
+
+ // Try to do the action if the command is enabled
+ gDownloadViewController.doCommand(menuitem.getAttribute("cmd"), item);
+}
+
+function removeFromView(aDownload)
+{
+ // Make sure we have an item to remove
+ if (!aDownload) return;
+
+ let index = gDownloadsView.selectedIndex;
+ gDownloadsView.removeChild(aDownload);
+ gDownloadsView.selectedIndex = Math.min(index, gDownloadsView.itemCount - 1);
+
+ // We might have removed the last item, so update the clear list button
+ updateClearListButton();
+}
+
+function getReferrerOrSource(aDownload)
+{
+ // Give the referrer if we have it set
+ if (aDownload.hasAttribute("referrer"))
+ return aDownload.getAttribute("referrer");
+
+ // Otherwise, provide the source
+ return aDownload.getAttribute("uri");
+}
+
+/**
+ * Initiate building the download list to have the active downloads followed by
+ * completed ones filtered by the search term if necessary.
+ *
+ * @param aForceBuild
+ * Force the list to be built even if the search terms don't change
+ */
+function buildDownloadList(aForceBuild)
+{
+ // Stringify the previous search
+ let prevSearch = gSearchTerms.join(" ");
+
+ // Array of space-separated lower-case search terms
+ gSearchTerms = gSearchBox.value.replace(/^\s+|\s+$/g, "").
+ toLowerCase().split(/\s+/);
+
+ // Unless forced, don't rebuild the download list if the search didn't change
+ if (!aForceBuild && gSearchTerms.join(" ") == prevSearch)
+ return;
+
+ // Clear out values before using them
+ clearTimeout(gBuilder);
+ gStmt.reset();
+
+ // Clear the list before adding items by replacing with a shallow copy
+ let empty = gDownloadsView.cloneNode(false);
+ gDownloadsView.parentNode.replaceChild(empty, gDownloadsView);
+ gDownloadsView = empty;
+
+ try {
+ gStmt.bindByIndex(0, nsIDM.DOWNLOAD_NOTSTARTED);
+ gStmt.bindByIndex(1, nsIDM.DOWNLOAD_DOWNLOADING);
+ gStmt.bindByIndex(2, nsIDM.DOWNLOAD_PAUSED);
+ gStmt.bindByIndex(3, nsIDM.DOWNLOAD_QUEUED);
+ gStmt.bindByIndex(4, nsIDM.DOWNLOAD_SCANNING);
+ } catch (e) {
+ // Something must have gone wrong when binding, so clear and quit
+ gStmt.reset();
+ return;
+ }
+
+ // Take a quick break before we actually start building the list
+ gBuilder = setTimeout(function() {
+ // Start building the list
+ stepListBuilder(1);
+
+ // We just tried to add a single item, so we probably need to enable
+ updateClearListButton();
+ }, 0);
+}
+
+/**
+ * Incrementally build the download list by adding at most the requested number
+ * of items if there are items to add. After doing that, it will schedule
+ * another chunk of items specified by gListBuildDelay and gListBuildChunk.
+ *
+ * @param aNumItems
+ * Number of items to add to the list before taking a break
+ */
+function stepListBuilder(aNumItems) {
+ try {
+ // If we're done adding all items, we can quit
+ if (!gStmt.executeStep()) {
+ // Send a notification that we finished, but wait for clear list to update
+ updateClearListButton();
+ setTimeout(() => Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService).
+ notifyObservers(window, "download-manager-ui-done", null), 0);
+
+ return;
+ }
+
+ // Try to get the attribute values from the statement
+ let attrs = {
+ dlid: gStmt.getInt64(0),
+ file: gStmt.getString(1),
+ target: gStmt.getString(2),
+ uri: gStmt.getString(3),
+ state: gStmt.getInt32(4),
+ startTime: Math.round(gStmt.getInt64(5) / 1000),
+ endTime: Math.round(gStmt.getInt64(6) / 1000),
+ currBytes: gStmt.getInt64(8),
+ maxBytes: gStmt.getInt64(9)
+ };
+
+ // Only add the referrer if it's not null
+ let referrer = gStmt.getString(7);
+ if (referrer)
+ attrs.referrer = referrer;
+
+ // If the download is active, grab the real progress, otherwise default 100
+ let isActive = gStmt.getInt32(10);
+ attrs.progress = isActive ? gDownloadManager.getDownload(attrs.dlid).
+ percentComplete : 100;
+
+ // Make the item and add it to the end if it's active or matches the search
+ let item = createDownloadItem(attrs);
+ if (item && (isActive || downloadMatchesSearch(item))) {
+ // Add item to the end
+ gDownloadsView.appendChild(item);
+
+ // Because of the joys of XBL, we can't update the buttons until the
+ // download object is in the document.
+ updateButtons(item);
+ } else {
+ // We didn't add an item, so bump up the number of items to process, but
+ // not a whole number so that we eventually do pause for a chunk break
+ aNumItems += .9;
+ }
+ } catch (e) {
+ // Something went wrong when stepping or getting values, so clear and quit
+ gStmt.reset();
+ return;
+ }
+
+ // Add another item to the list if we should; otherwise, let the UI update
+ // and continue later
+ if (aNumItems > 1) {
+ stepListBuilder(aNumItems - 1);
+ } else {
+ // Use a shorter delay for earlier downloads to display them faster
+ let delay = Math.min(gDownloadsView.itemCount * 10, gListBuildDelay);
+ gBuilder = setTimeout(stepListBuilder, delay, gListBuildChunk);
+ }
+}
+
+/**
+ * Add a download to the front of the download list
+ *
+ * @param aDownload
+ * The nsIDownload to make into a richlistitem
+ */
+function prependList(aDownload)
+{
+ let attrs = {
+ dlid: aDownload.id,
+ file: aDownload.target.spec,
+ target: aDownload.displayName,
+ uri: aDownload.source.spec,
+ state: aDownload.state,
+ progress: aDownload.percentComplete,
+ startTime: Math.round(aDownload.startTime / 1000),
+ endTime: Date.now(),
+ currBytes: aDownload.amountTransferred,
+ maxBytes: aDownload.size
+ };
+
+ // Make the item and add it to the beginning
+ let item = createDownloadItem(attrs);
+ if (item) {
+ // Add item to the beginning
+ gDownloadsView.insertBefore(item, gDownloadsView.firstChild);
+
+ // Because of the joys of XBL, we can't update the buttons until the
+ // download object is in the document.
+ updateButtons(item);
+
+ // We might have added an item to an empty list, so update button
+ updateClearListButton();
+ }
+}
+
+/**
+ * Check if the download matches the current search term based on the texts
+ * shown to the user. All search terms are checked to see if each matches any
+ * of the displayed texts.
+ *
+ * @param aItem
+ * Download richlistitem to check if it matches the current search
+ * @return Boolean true if it matches the search; false otherwise
+ */
+function downloadMatchesSearch(aItem)
+{
+ // Search through the download attributes that are shown to the user and
+ // make it into one big string for easy combined searching
+ let combinedSearch = "";
+ for (let attr of gSearchAttributes)
+ combinedSearch += aItem.getAttribute(attr).toLowerCase() + " ";
+
+ // Make sure each of the terms are found
+ for (let term of gSearchTerms)
+ if (combinedSearch.indexOf(term) == -1)
+ return false;
+
+ return true;
+}
+
+// we should be using real URLs all the time, but until
+// bug 239948 is fully fixed, this will do...
+//
+// note, this will thrown an exception if the native path
+// is not valid (for example a native Windows path on a Mac)
+// see bug #392386 for details
+function getLocalFileFromNativePathOrUrl(aPathOrUrl)
+{
+ if (aPathOrUrl.substring(0, 7) == "file://") {
+ // if this is a URL, get the file from that
+ let ioSvc = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+
+ // XXX it's possible that using a null char-set here is bad
+ const fileUrl = ioSvc.newURI(aPathOrUrl, null, null).
+ QueryInterface(Ci.nsIFileURL);
+ return fileUrl.file.clone().QueryInterface(Ci.nsILocalFile);
+ }
+ // if it's a pathname, create the nsILocalFile directly
+ var f = new nsLocalFile(aPathOrUrl);
+
+ return f;
+}
+
+/**
+ * Update the disabled state of the clear list button based on whether or not
+ * there are items in the list that can potentially be removed.
+ */
+function updateClearListButton()
+{
+ let button = document.getElementById("clearListButton");
+ // The button is enabled if we have items in the list and we can clean up
+ button.disabled = !(gDownloadsView.itemCount && gDownloadManager.canCleanUp);
+}
+
+function getDownload(aID)
+{
+ return document.getElementById("dl" + aID);
+}
+
+/**
+ * Initialize the statement which is used to retrieve the list of downloads.
+ */
+function initStatement()
+{
+ if (gStmt)
+ gStmt.finalize();
+
+ gStmt = gDownloadManager.DBConnection.createStatement(
+ "SELECT id, target, name, source, state, startTime, endTime, referrer, " +
+ "currBytes, maxBytes, state IN (?1, ?2, ?3, ?4, ?5) isActive " +
+ "FROM moz_downloads " +
+ "ORDER BY isActive DESC, endTime DESC, startTime DESC");
+}
diff --git a/toolkit/mozapps/downloads/content/downloads.xul b/toolkit/mozapps/downloads/content/downloads.xul
new file mode 100644
index 000000000..5ca9eec2d
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/downloads.xul
@@ -0,0 +1,164 @@
+<?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/.
+
+#ifdef XP_UNIX
+#ifndef XP_MACOSX
+#define XP_GNOME 1
+#endif
+#endif
+
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/content/downloads/downloads.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/downloads/downloads.css"?>
+
+<!DOCTYPE window [
+<!ENTITY % downloadManagerDTD SYSTEM "chrome://mozapps/locale/downloads/downloads.dtd">
+%downloadManagerDTD;
+<!ENTITY % editMenuOverlayDTD SYSTEM "chrome://global/locale/editMenuOverlay.dtd">
+%editMenuOverlayDTD;
+]>
+
+<window xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="downloadManager" windowtype="Download:Manager"
+ orient="vertical" title="&downloads.title;" statictitle="&downloads.title;"
+ width="&window.width2;" height="&window.height;" screenX="10" screenY="10"
+ persist="width height screenX screenY sizemode"
+ onload="Startup();" onunload="Shutdown();"
+ onclose="return closeWindow(false);">
+
+ <script type="application/javascript" src="chrome://mozapps/content/downloads/downloads.js"/>
+ <script type="application/javascript" src="chrome://mozapps/content/downloads/DownloadProgressListener.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+
+ <stringbundleset id="downloadSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="downloadStrings" src="chrome://mozapps/locale/downloads/downloads.properties"/>
+ </stringbundleset>
+
+ <!-- Use this commandset for command which do not depened on focus or selection -->
+ <commandset id="generalCommands">
+ <command id="cmd_findDownload" oncommand="setSearchboxFocus();"/>
+ <command id="cmd_selectAllDownloads" oncommand="gDownloadsView.selectAll();"/>
+ <command id="cmd_clearList" oncommand="clearDownloadList();"/>
+ </commandset>
+
+ <keyset id="downloadKeys">
+ <key keycode="VK_RETURN" oncommand="doDefaultForSelected();"/>
+ <key id="key_pauseResume" key=" " oncommand="performCommand('cmd_pauseResume');"/>
+ <key id="key_removeFromList" keycode="VK_DELETE" oncommand="performCommand('cmd_removeFromList');"/>
+#ifdef XP_MACOSX
+ <key id="key_removeFromList2" keycode="VK_BACK" oncommand="performCommand('cmd_removeFromList');"/>
+#endif
+ <key id="key_close" key="&cmd.close.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/>
+#ifdef XP_GNOME
+ <key id="key_close2" key="&cmd.close2Unix.commandKey;" oncommand="closeWindow(true);" modifiers="accel,shift"/>
+#else
+ <key id="key_close2" key="&cmd.close2.commandKey;" oncommand="closeWindow(true);" modifiers="accel"/>
+#endif
+ <key keycode="VK_ESCAPE" oncommand="closeWindow(true);"/>
+
+ <key id="key_findDownload"
+ key="&cmd.find.commandKey;"
+ modifiers="accel"
+ command="cmd_findDownload"/>
+ <key id="key_findDownload2"
+ key="&cmd.search.commandKey;"
+ modifiers="accel"
+ command="cmd_findDownload"/>
+ <key id="key_selectAllDownloads"
+ key="&selectAllCmd.key;"
+ modifiers="accel"
+ command="cmd_selectAllDownloads"/>
+ <key id="pasteKey"
+ key="V"
+ modifiers="accel"
+ oncommand="pasteHandler();"/>
+ </keyset>
+
+ <vbox id="contextMenuPalette" hidden="true">
+ <menuitem id="menuitem_pause"
+ label="&cmd.pause.label;" accesskey="&cmd.pause.accesskey;"
+ oncommand="performCommand('cmd_pause');"
+ cmd="cmd_pause"/>
+ <menuitem id="menuitem_resume"
+ label="&cmd.resume.label;" accesskey="&cmd.resume.accesskey;"
+ oncommand="performCommand('cmd_resume');"
+ cmd="cmd_resume"/>
+ <menuitem id="menuitem_cancel"
+ label="&cmd.cancel.label;" accesskey="&cmd.cancel.accesskey;"
+ oncommand="performCommand('cmd_cancel');"
+ cmd="cmd_cancel"/>
+
+ <menuitem id="menuitem_open" default="true"
+ label="&cmd.open.label;" accesskey="&cmd.open.accesskey;"
+ oncommand="performCommand('cmd_open');"
+ cmd="cmd_open"/>
+ <menuitem id="menuitem_show"
+#ifdef XP_MACOSX
+ label="&cmd.showMac.label;"
+ accesskey="&cmd.showMac.accesskey;"
+#else
+ label="&cmd.show.label;"
+ accesskey="&cmd.show.accesskey;"
+#endif
+ oncommand="performCommand('cmd_show');"
+ cmd="cmd_show"/>
+
+ <menuitem id="menuitem_retry" default="true"
+ label="&cmd.retry.label;" accesskey="&cmd.retry.accesskey;"
+ oncommand="performCommand('cmd_retry');"
+ cmd="cmd_retry"/>
+
+ <menuitem id="menuitem_removeFromList"
+ label="&cmd.removeFromList.label;" accesskey="&cmd.removeFromList.accesskey;"
+ oncommand="performCommand('cmd_removeFromList');"
+ cmd="cmd_removeFromList"/>
+
+ <menuseparator id="menuseparator"/>
+
+ <menuitem id="menuitem_openReferrer"
+ label="&cmd.goToDownloadPage.label;"
+ accesskey="&cmd.goToDownloadPage.accesskey;"
+ oncommand="performCommand('cmd_openReferrer');"
+ cmd="cmd_openReferrer"/>
+
+ <menuitem id="menuitem_copyLocation"
+ label="&cmd.copyDownloadLink.label;"
+ accesskey="&cmd.copyDownloadLink.accesskey;"
+ oncommand="performCommand('cmd_copyLocation');"
+ cmd="cmd_copyLocation"/>
+
+ <menuitem id="menuitem_selectAll"
+ label="&selectAllCmd.label;"
+ accesskey="&selectAllCmd.accesskey;"
+ command="cmd_selectAllDownloads"/>
+ </vbox>
+
+ <menupopup id="downloadContextMenu" onpopupshowing="return buildContextMenu(event);"/>
+
+ <richlistbox id="downloadView" seltype="multiple" flex="1"
+ context="downloadContextMenu"
+ ondblclick="onDownloadDblClick(event);"
+ ondragstart="gDownloadDNDObserver.onDragStart(event);"
+ ondragover="gDownloadDNDObserver.onDragOver(event);event.stopPropagation();"
+ ondrop="gDownloadDNDObserver.onDrop(event)">
+ </richlistbox>
+
+ <windowdragbox id="search" align="center">
+ <button id="clearListButton" command="cmd_clearList"
+ label="&cmd.clearList.label;"
+ accesskey="&cmd.clearList.accesskey;"
+ tooltiptext="&cmd.clearList.tooltip;"/>
+ <spacer flex="1"/>
+ <textbox type="search" id="searchbox" class="compact"
+ aria-controls="downloadView"
+ oncommand="buildDownloadList();" placeholder="&searchBox.label;"/>
+ </windowdragbox>
+
+</window>
diff --git a/toolkit/mozapps/downloads/content/unknownContentType.xul b/toolkit/mozapps/downloads/content/unknownContentType.xul
new file mode 100644
index 000000000..af8b7b016
--- /dev/null
+++ b/toolkit/mozapps/downloads/content/unknownContentType.xul
@@ -0,0 +1,107 @@
+<?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/downloads/unknownContentType.css" type="text/css"?>
+
+<!DOCTYPE dialog [
+ <!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+ %brandDTD;
+ <!ENTITY % uctDTD SYSTEM "chrome://mozapps/locale/downloads/unknownContentType.dtd" >
+ %uctDTD;
+ <!ENTITY % scDTD SYSTEM "chrome://mozapps/locale/downloads/settingsChange.dtd" >
+ %scDTD;
+]>
+
+<dialog id="unknownContentType"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="dialog.initDialog();" onunload="if (dialog) dialog.onCancel();"
+#ifdef XP_WIN
+ style="width: 36em;"
+#else
+ style="width: 34em;"
+#endif
+ screenX="" screenY=""
+ persist="screenX screenY"
+ aria-describedby="intro location whichIs type from source unknownPrompt"
+ ondialogaccept="return dialog.onOK()"
+ ondialogcancel="return dialog.onCancel()">
+
+
+ <stringbundle id="strings" src="chrome://mozapps/locale/downloads/unknownContentType.properties"/>
+
+ <vbox flex="1" id="container">
+ <description id="intro">&intro2.label;</description>
+ <separator class="thin"/>
+ <hbox align="start" class="small-indent">
+ <image id="contentTypeImage"/>
+ <vbox flex="1">
+ <description id="location" class="plain" crop="start" flex="1"/>
+ <separator class="thin"/>
+ <hbox align="center">
+ <label id="whichIs" value="&whichIs.label;"/>
+ <textbox id="type" class="plain" readonly="true" flex="1" noinitialfocus="true"/>
+ </hbox>
+ <hbox align="center">
+ <label value="&from.label;" id="from"/>
+ <description id="source" class="plain" crop="start" flex="1"/>
+ </hbox>
+ </vbox>
+ </hbox>
+
+ <separator class="thin"/>
+
+ <hbox align="center" id="basicBox" collapsed="true">
+ <label id="unknownPrompt" value="&unknownPromptText.label;" flex="1"/>
+ </hbox>
+
+ <groupbox flex="1" id="normalBox">
+ <caption label="&actionQuestion.label;"/>
+ <separator class="thin"/>
+ <radiogroup id="mode" class="small-indent">
+ <hbox>
+ <radio id="open" label="&openWith.label;" accesskey="&openWith.accesskey;"/>
+ <deck id="modeDeck" flex="1">
+ <hbox id="openHandlerBox" flex="1" align="center"/>
+ <hbox flex="1" align="center">
+ <button id="chooseButton" oncommand="dialog.chooseApp();"
+#ifdef XP_MACOSX
+ label="&chooseHandlerMac.label;" accesskey="&chooseHandlerMac.accesskey;"/>
+#else
+ label="&chooseHandler.label;" accesskey="&chooseHandler.accesskey;"/>
+#endif
+ </hbox>
+ </deck>
+ </hbox>
+
+ <radio id="save" label="&saveFile.label;" accesskey="&saveFile.accesskey;"/>
+ </radiogroup>
+ <separator class="thin"/>
+ <hbox class="small-indent">
+ <checkbox id="rememberChoice" label="&rememberChoice.label;"
+ accesskey="&rememberChoice.accesskey;"
+ oncommand="dialog.toggleRememberChoice(event.target);"/>
+ </hbox>
+
+ <separator/>
+#ifdef XP_UNIX
+ <description id="settingsChange" hidden="true">&settingsChangePreferences.label;</description>
+#else
+ <description id="settingsChange" hidden="true">&settingsChangeOptions.label;</description>
+#endif
+ <separator class="thin"/>
+ </groupbox>
+ </vbox>
+
+ <menulist id="openHandler" flex="1">
+ <menupopup id="openHandlerPopup" oncommand="dialog.openHandlerCommand();">
+ <menuitem id="defaultHandler" default="true" crop="right"/>
+ <menuitem id="otherHandler" hidden="true" crop="left"/>
+ <menuseparator/>
+ <menuitem id="choose" label="&other.label;"/>
+ </menupopup>
+ </menulist>
+</dialog>
diff --git a/toolkit/mozapps/downloads/jar.mn b/toolkit/mozapps/downloads/jar.mn
new file mode 100644
index 000000000..eb761b0d9
--- /dev/null
+++ b/toolkit/mozapps/downloads/jar.mn
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+* content/mozapps/downloads/unknownContentType.xul (content/unknownContentType.xul)
+* content/mozapps/downloads/downloads.xul (content/downloads.xul)
+ content/mozapps/downloads/downloads.js (content/downloads.js)
+ content/mozapps/downloads/DownloadProgressListener.js (content/DownloadProgressListener.js)
+ content/mozapps/downloads/downloads.css (content/downloads.css)
+ content/mozapps/downloads/download.xml (content/download.xml)
diff --git a/toolkit/mozapps/downloads/moz.build b/toolkit/mozapps/downloads/moz.build
new file mode 100644
index 000000000..1850ea7de
--- /dev/null
+++ b/toolkit/mozapps/downloads/moz.build
@@ -0,0 +1,24 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+TEST_DIRS += ['tests']
+
+EXTRA_COMPONENTS += [
+ 'nsHelperAppDlg.manifest',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'nsHelperAppDlg.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'DownloadLastDir.jsm',
+ 'DownloadPaths.jsm',
+ 'DownloadTaskbarProgress.jsm',
+ 'DownloadUtils.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.js b/toolkit/mozapps/downloads/nsHelperAppDlg.js
new file mode 100644
index 000000000..58697cc77
--- /dev/null
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.js
@@ -0,0 +1,1147 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const {utils: Cu, interfaces: Ci, classes: Cc, results: Cr} = Components;
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "EnableDelayHelper",
+ "resource://gre/modules/SharedPromptUtils.jsm");
+
+///////////////////////////////////////////////////////////////////////////////
+//// Helper Functions
+
+/**
+ * Determines if a given directory is able to be used to download to.
+ *
+ * @param aDirectory
+ * The directory to check.
+ * @return true if we can use the directory, false otherwise.
+ */
+function isUsableDirectory(aDirectory)
+{
+ return aDirectory.exists() && aDirectory.isDirectory() &&
+ aDirectory.isWritable();
+}
+
+// Web progress listener so we can detect errors while mLauncher is
+// streaming the data to a temporary file.
+function nsUnknownContentTypeDialogProgressListener(aHelperAppDialog) {
+ this.helperAppDlg = aHelperAppDialog;
+}
+
+nsUnknownContentTypeDialogProgressListener.prototype = {
+ // nsIWebProgressListener methods.
+ // Look for error notifications and display alert to user.
+ onStatusChange: function( aWebProgress, aRequest, aStatus, aMessage ) {
+ if ( aStatus != Components.results.NS_OK ) {
+ // Display error alert (using text supplied by back-end).
+ // FIXME this.dialog is undefined?
+ Services.prompt.alert( this.dialog, this.helperAppDlg.mTitle, aMessage );
+ // Close the dialog.
+ this.helperAppDlg.onCancel();
+ if ( this.helperAppDlg.mDialog ) {
+ this.helperAppDlg.mDialog.close();
+ }
+ }
+ },
+
+ // Ignore onProgressChange, onProgressChange64, onStateChange, onLocationChange, onSecurityChange, and onRefreshAttempted notifications.
+ onProgressChange: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+ onProgressChange64: function( aWebProgress,
+ aRequest,
+ aCurSelfProgress,
+ aMaxSelfProgress,
+ aCurTotalProgress,
+ aMaxTotalProgress ) {
+ },
+
+
+
+ onStateChange: function( aWebProgress, aRequest, aStateFlags, aStatus ) {
+ },
+
+ onLocationChange: function( aWebProgress, aRequest, aLocation, aFlags ) {
+ },
+
+ onSecurityChange: function( aWebProgress, aRequest, state ) {
+ },
+
+ onRefreshAttempted: function( aWebProgress, aURI, aDelay, aSameURI ) {
+ return true;
+ }
+};
+
+///////////////////////////////////////////////////////////////////////////////
+//// nsUnknownContentTypeDialog
+
+/* This file implements the nsIHelperAppLauncherDialog interface.
+ *
+ * The implementation consists of a JavaScript "class" named nsUnknownContentTypeDialog,
+ * comprised of:
+ * - a JS constructor function
+ * - a prototype providing all the interface methods and implementation stuff
+ *
+ * In addition, this file implements an nsIModule object that registers the
+ * nsUnknownContentTypeDialog component.
+ */
+
+const PREF_BD_USEDOWNLOADDIR = "browser.download.useDownloadDir";
+const nsITimer = Components.interfaces.nsITimer;
+
+var downloadModule = {};
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/DownloadLastDir.jsm", downloadModule);
+Components.utils.import("resource://gre/modules/DownloadPaths.jsm");
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+Components.utils.import("resource://gre/modules/Downloads.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+
+/* ctor
+ */
+function nsUnknownContentTypeDialog() {
+ // Initialize data properties.
+ this.mLauncher = null;
+ this.mContext = null;
+ this.mReason = null;
+ this.chosenApp = null;
+ this.givenDefaultApp = false;
+ this.updateSelf = true;
+ this.mTitle = "";
+}
+
+nsUnknownContentTypeDialog.prototype = {
+ classID: Components.ID("{F68578EB-6EC2-4169-AE19-8C6243F0ABE1}"),
+
+ nsIMIMEInfo : Components.interfaces.nsIMIMEInfo,
+
+ QueryInterface: function (iid) {
+ if (!iid.equals(Components.interfaces.nsIHelperAppLauncherDialog) &&
+ !iid.equals(Components.interfaces.nsITimerCallback) &&
+ !iid.equals(Components.interfaces.nsISupports)) {
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ return this;
+ },
+
+ // ---------- nsIHelperAppLauncherDialog methods ----------
+
+ // show: Open XUL dialog using window watcher. Since the dialog is not
+ // modal, it needs to be a top level window and the way to open
+ // one of those is via that route).
+ show: function(aLauncher, aContext, aReason) {
+ this.mLauncher = aLauncher;
+ this.mContext = aContext;
+ this.mReason = aReason;
+
+ // Cache some information in case this context goes away:
+ try {
+ let parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ this._mDownloadDir = new downloadModule.DownloadLastDir(parent);
+ } catch (ex) {
+ Cu.reportError("Missing window information when showing nsIHelperAppLauncherDialog: " + ex);
+ }
+
+ const nsITimer = Components.interfaces.nsITimer;
+ this._showTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._showTimer.initWithCallback(this, 0, nsITimer.TYPE_ONE_SHOT);
+ },
+
+ // When opening from new tab, if tab closes while dialog is opening,
+ // (which is a race condition on the XUL file being cached and the timer
+ // in nsExternalHelperAppService), the dialog gets a blur and doesn't
+ // activate the OK button. So we wait a bit before doing opening it.
+ reallyShow: function() {
+ try {
+ let ir = this.mContext.QueryInterface(Components.interfaces.nsIInterfaceRequestor);
+ let docShell = ir.getInterface(Components.interfaces.nsIDocShell);
+ let rootWin = docShell.QueryInterface(Ci.nsIDocShellTreeItem)
+ .rootTreeItem
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ let ww = Components.classes["@mozilla.org/embedcomp/window-watcher;1"]
+ .getService(Components.interfaces.nsIWindowWatcher);
+ this.mDialog = ww.openWindow(rootWin,
+ "chrome://mozapps/content/downloads/unknownContentType.xul",
+ null,
+ "chrome,centerscreen,titlebar,dialog=yes,dependent",
+ null);
+ } catch (ex) {
+ // The containing window may have gone away. Break reference
+ // cycles and stop doing the download.
+ this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
+ return;
+ }
+
+ // Hook this object to the dialog.
+ this.mDialog.dialog = this;
+
+ // Hook up utility functions.
+ this.getSpecialFolderKey = this.mDialog.getSpecialFolderKey;
+
+ // Watch for error notifications.
+ var progressListener = new nsUnknownContentTypeDialogProgressListener(this);
+ this.mLauncher.setWebProgressListener(progressListener);
+ },
+
+ //
+ // displayBadPermissionAlert()
+ //
+ // Diplay an alert panel about the bad permission of folder/directory.
+ //
+ displayBadPermissionAlert: function () {
+ let bundle =
+ Services.strings.createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
+
+ Services.prompt.alert(this.dialog,
+ bundle.GetStringFromName("badPermissions.title"),
+ bundle.GetStringFromName("badPermissions"));
+ },
+
+ promptForSaveToFileAsync: function(aLauncher, aContext, aDefaultFile, aSuggestedFileExtension, aForcePrompt) {
+ var result = null;
+
+ this.mLauncher = aLauncher;
+
+ let prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ let bundle =
+ Services.strings
+ .createBundle("chrome://mozapps/locale/downloads/unknownContentType.properties");
+
+ let parent;
+ let gDownloadLastDir;
+ try {
+ parent = aContext.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ } catch (ex) {}
+
+ if (parent) {
+ gDownloadLastDir = new downloadModule.DownloadLastDir(parent);
+ } else {
+ // Use the cached download info, but pick an arbitrary parent window
+ // because the original one is definitely gone (and nsIFilePicker doesn't like
+ // a null parent):
+ gDownloadLastDir = this._mDownloadDir;
+ let windowsEnum = Services.wm.getEnumerator("");
+ while (windowsEnum.hasMoreElements()) {
+ let someWin = windowsEnum.getNext();
+ // We need to make sure we don't end up with this dialog, because otherwise
+ // that's going to go away when the user clicks "Save", and that breaks the
+ // windows file picker that's supposed to show up if we let the user choose
+ // where to save files...
+ if (someWin != this.mDialog) {
+ parent = someWin;
+ }
+ }
+ if (!parent) {
+ Cu.reportError("No candidate parent windows were found for the save filepicker." +
+ "This should never happen.");
+ }
+ }
+
+ Task.spawn(function() {
+ if (!aForcePrompt) {
+ // Check to see if the user wishes to auto save to the default download
+ // folder without prompting. Note that preference might not be set.
+ let autodownload = false;
+ try {
+ autodownload = prefs.getBoolPref(PREF_BD_USEDOWNLOADDIR);
+ } catch (e) { }
+
+ if (autodownload) {
+ // Retrieve the user's default download directory
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ let defaultFolder = new FileUtils.File(preferredDir);
+
+ try {
+ result = this.validateLeafName(defaultFolder, aDefaultFile, aSuggestedFileExtension);
+ }
+ catch (ex) {
+ // When the default download directory is write-protected,
+ // prompt the user for a different target file.
+ }
+
+ // Check to make sure we have a valid directory, otherwise, prompt
+ if (result) {
+ // This path is taken when we have a writable default download directory.
+ aLauncher.saveDestinationAvailable(result);
+ return;
+ }
+ }
+ }
+
+ // Use file picker to show dialog.
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var picker = Components.classes["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
+ var windowTitle = bundle.GetStringFromName("saveDialogTitle");
+ picker.init(parent, windowTitle, nsIFilePicker.modeSave);
+ picker.defaultString = aDefaultFile;
+
+ if (aSuggestedFileExtension) {
+ // aSuggestedFileExtension includes the period, so strip it
+ picker.defaultExtension = aSuggestedFileExtension.substring(1);
+ }
+ else {
+ try {
+ picker.defaultExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ }
+ catch (ex) { }
+ }
+
+ var wildCardExtension = "*";
+ if (aSuggestedFileExtension) {
+ wildCardExtension += aSuggestedFileExtension;
+ picker.appendFilter(this.mLauncher.MIMEInfo.description, wildCardExtension);
+ }
+
+ picker.appendFilters( nsIFilePicker.filterAll );
+
+ // Default to lastDir if it is valid, otherwise use the user's default
+ // downloads directory. getPreferredDownloadsDirectory should always
+ // return a valid directory path, so we can safely default to it.
+ let preferredDir = yield Downloads.getPreferredDownloadsDirectory();
+ picker.displayDirectory = new FileUtils.File(preferredDir);
+
+ gDownloadLastDir.getFileAsync(aLauncher.source, function LastDirCallback(lastDir) {
+ if (lastDir && isUsableDirectory(lastDir))
+ picker.displayDirectory = lastDir;
+
+ if (picker.show() == nsIFilePicker.returnCancel) {
+ // null result means user cancelled.
+ aLauncher.saveDestinationAvailable(null);
+ return;
+ }
+
+ // Be sure to save the directory the user chose through the Save As...
+ // dialog as the new browser.download.dir since the old one
+ // didn't exist.
+ result = picker.file;
+
+ if (result) {
+ try {
+ // Remove the file so that it's not there when we ensure non-existence later;
+ // this is safe because for the file to exist, the user would have had to
+ // confirm that he wanted the file overwritten.
+ // Only remove file if final name exists
+ if (result.exists() && this.getFinalLeafName(result.leafName) == result.leafName)
+ result.remove(false);
+ }
+ catch (ex) {
+ // As it turns out, the failure to remove the file, for example due to
+ // permission error, will be handled below eventually somehow.
+ }
+
+ var newDir = result.parent.QueryInterface(Components.interfaces.nsILocalFile);
+
+ // Do not store the last save directory as a pref inside the private browsing mode
+ gDownloadLastDir.setFile(aLauncher.source, newDir);
+
+ try {
+ result = this.validateLeafName(newDir, result.leafName, null);
+ }
+ catch (ex) {
+ // When the chosen download directory is write-protected,
+ // display an informative error message.
+ // In all cases, download will be stopped.
+
+ if (ex.result == Components.results.NS_ERROR_FILE_ACCESS_DENIED) {
+ this.displayBadPermissionAlert();
+ aLauncher.saveDestinationAvailable(null);
+ return;
+ }
+
+ }
+ }
+ aLauncher.saveDestinationAvailable(result);
+ }.bind(this));
+ }.bind(this)).then(null, Components.utils.reportError);
+ },
+
+ getFinalLeafName: function (aLeafName, aFileExt)
+ {
+ // Remove any leading periods, since we don't want to save hidden files
+ // automatically.
+ aLeafName = aLeafName.replace(/^\.+/, "");
+
+ if (aLeafName == "")
+ aLeafName = "unnamed" + (aFileExt ? "." + aFileExt : "");
+
+ return aLeafName;
+ },
+
+ /**
+ * Ensures that a local folder/file combination does not already exist in
+ * the file system (or finds such a combination with a reasonably similar
+ * leaf name), creates the corresponding file, and returns it.
+ *
+ * @param aLocalFolder
+ * the folder where the file resides
+ * @param aLeafName
+ * the string name of the file (may be empty if no name is known,
+ * in which case a name will be chosen)
+ * @param aFileExt
+ * the extension of the file, if one is known; this will be ignored
+ * if aLeafName is non-empty
+ * @return nsILocalFile
+ * the created file
+ * @throw an error such as permission doesn't allow creation of
+ * file, etc.
+ */
+ validateLeafName: function (aLocalFolder, aLeafName, aFileExt)
+ {
+ if (!(aLocalFolder && isUsableDirectory(aLocalFolder))) {
+ throw new Components.Exception("Destination directory non-existing or permission error",
+ Components.results.NS_ERROR_FILE_ACCESS_DENIED);
+ }
+
+ aLeafName = this.getFinalLeafName(aLeafName, aFileExt);
+ aLocalFolder.append(aLeafName);
+
+ // The following assignment can throw an exception, but
+ // is now caught properly in the caller of validateLeafName.
+ var createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
+
+ if (AppConstants.platform == "win") {
+ let ext;
+ try {
+ // We can fail here if there's no primary extension set
+ ext = "." + this.mLauncher.MIMEInfo.primaryExtension;
+ } catch (e) { }
+
+ // Append a file extension if it's an executable that doesn't have one
+ // but make sure we actually have an extension to add
+ let leaf = createdFile.leafName;
+ if (ext && leaf.slice(-ext.length) != ext && createdFile.isExecutable()) {
+ createdFile.remove(false);
+ aLocalFolder.leafName = leaf + ext;
+ createdFile = DownloadPaths.createNiceUniqueFile(aLocalFolder);
+ }
+ }
+
+ return createdFile;
+ },
+
+ // ---------- implementation methods ----------
+
+ // initDialog: Fill various dialog fields with initial content.
+ initDialog : function() {
+ // Put file name in window title.
+ var suggestedFileName = this.mLauncher.suggestedFileName;
+
+ // Some URIs do not implement nsIURL, so we can't just QI.
+ var url = this.mLauncher.source;
+ if (url instanceof Components.interfaces.nsINestedURI)
+ url = url.innermostURI;
+
+ var fname = "";
+ var iconPath = "goat";
+ this.mSourcePath = url.prePath;
+ if (url instanceof Components.interfaces.nsIURL) {
+ // A url, use file name from it.
+ fname = iconPath = url.fileName;
+ this.mSourcePath += url.directory;
+ } else {
+ // A generic uri, use path.
+ fname = url.path;
+ this.mSourcePath += url.path;
+ }
+
+ if (suggestedFileName)
+ fname = iconPath = suggestedFileName;
+
+ var displayName = fname.replace(/ +/g, " ");
+
+ this.mTitle = this.dialogElement("strings").getFormattedString("title", [displayName]);
+ this.mDialog.document.title = this.mTitle;
+
+ // Put content type, filename and location into intro.
+ this.initIntro(url, fname, displayName);
+
+ var iconString = "moz-icon://" + iconPath + "?size=16&contentType=" + this.mLauncher.MIMEInfo.MIMEType;
+ this.dialogElement("contentTypeImage").setAttribute("src", iconString);
+
+ // if always-save and is-executable and no-handler
+ // then set up simple ui
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ var shouldntRememberChoice = (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload" ||
+ this.mLauncher.targetFileIsExecutable);
+ if ((shouldntRememberChoice && !this.openWithDefaultOK()) ||
+ Services.prefs.getBoolPref("browser.download.forbid_open_with")) {
+ // hide featured choice
+ this.dialogElement("normalBox").collapsed = true;
+ // show basic choice
+ this.dialogElement("basicBox").collapsed = false;
+ // change button labels and icons; use "save" icon for the accept
+ // button since it's the only action possible
+ let acceptButton = this.mDialog.document.documentElement
+ .getButton("accept");
+ acceptButton.label = this.dialogElement("strings")
+ .getString("unknownAccept.label");
+ acceptButton.setAttribute("icon", "save");
+ this.mDialog.document.documentElement.getButton("cancel").label = this.dialogElement("strings").getString("unknownCancel.label");
+ // hide other handler
+ this.dialogElement("openHandler").collapsed = true;
+ // set save as the selected option
+ this.dialogElement("mode").selectedItem = this.dialogElement("save");
+ }
+ else {
+ this.initAppAndSaveToDiskValues();
+
+ // Initialize "always ask me" box. This should always be disabled
+ // and set to true for the ambiguous type application/octet-stream.
+ // We don't also check for application/x-msdownload here since we
+ // want users to be able to autodownload .exe files.
+ var rememberChoice = this.dialogElement("rememberChoice");
+
+ // Just because we have a content-type of application/octet-stream
+ // here doesn't actually mean that the content is of that type. Many
+ // servers default to sending text/plain for file types they don't know
+ // about. To account for this, the uriloader does some checking to see
+ // if a file sent as text/plain contains binary characters, and if so (*)
+ // it morphs the content-type into application/octet-stream so that
+ // the file can be properly handled. Since this is not generic binary
+ // data, rather, a data format that the system probably knows about,
+ // we don't want to use the content-type provided by this dialog's
+ // opener, as that's the generic application/octet-stream that the
+ // uriloader has passed, rather we want to ask the MIME Service.
+ // This is so we don't needlessly disable the "autohandle" checkbox.
+
+ // commented out to close the opening brace in the if statement.
+ // var mimeService = Components.classes["@mozilla.org/mime;1"].getService(Components.interfaces.nsIMIMEService);
+ // var type = mimeService.getTypeFromURI(this.mLauncher.source);
+ // this.realMIMEInfo = mimeService.getFromTypeAndExtension(type, "");
+
+ // if (type == "application/octet-stream") {
+ if (shouldntRememberChoice) {
+ rememberChoice.checked = false;
+ rememberChoice.disabled = true;
+ }
+ else {
+ rememberChoice.checked = !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling &&
+ this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.handleInternally;
+ }
+ this.toggleRememberChoice(rememberChoice);
+
+ // XXXben - menulist won't init properly, hack.
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.parentNode.removeChild(openHandler);
+ var openHandlerBox = this.dialogElement("openHandlerBox");
+ openHandlerBox.appendChild(openHandler);
+ }
+
+ this.mDialog.setTimeout("dialog.postShowCallback()", 0);
+
+ this.delayHelper = new EnableDelayHelper({
+ disableDialog: () => {
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ },
+ enableDialog: () => {
+ this.mDialog.document.documentElement.getButton("accept").disabled = false;
+ },
+ focusTarget: this.mDialog
+ });
+ },
+
+ notify: function (aTimer) {
+ if (aTimer == this._showTimer) {
+ if (!this.mDialog) {
+ this.reallyShow();
+ }
+ // The timer won't release us, so we have to release it.
+ this._showTimer = null;
+ }
+ else if (aTimer == this._saveToDiskTimer) {
+ // Since saveToDisk may open a file picker and therefore block this routine,
+ // we should only call it once the dialog is closed.
+ this.mLauncher.saveToDisk(null, false);
+ this._saveToDiskTimer = null;
+ }
+ },
+
+ postShowCallback: function () {
+ this.mDialog.sizeToContent();
+
+ // Set initial focus
+ this.dialogElement("mode").focus();
+ },
+
+ // initIntro:
+ initIntro: function(url, filename, displayname) {
+ this.dialogElement( "location" ).value = displayname;
+ this.dialogElement( "location" ).setAttribute("realname", filename);
+ this.dialogElement( "location" ).setAttribute("tooltiptext", displayname);
+
+ // if mSourcePath is a local file, then let's use the pretty path name
+ // instead of an ugly url...
+ var pathString;
+ if (url instanceof Components.interfaces.nsIFileURL) {
+ try {
+ // Getting .file might throw, or .parent could be null
+ pathString = url.file.parent.path;
+ } catch (ex) {}
+ }
+
+ if (!pathString) {
+ // wasn't a fileURL
+ var tmpurl = url.clone(); // don't want to change the real url
+ try {
+ tmpurl.userPass = "";
+ } catch (ex) {}
+ pathString = tmpurl.prePath;
+ }
+
+ // Set the location text, which is separate from the intro text so it can be cropped
+ var location = this.dialogElement( "source" );
+ location.value = pathString;
+ location.setAttribute("tooltiptext", this.mSourcePath);
+
+ // Show the type of file.
+ var type = this.dialogElement("type");
+ var mimeInfo = this.mLauncher.MIMEInfo;
+
+ // 1. Try to use the pretty description of the type, if one is available.
+ var typeString = mimeInfo.description;
+
+ if (typeString == "") {
+ // 2. If there is none, use the extension to identify the file, e.g. "ZIP file"
+ var primaryExtension = "";
+ try {
+ primaryExtension = mimeInfo.primaryExtension;
+ }
+ catch (ex) {
+ }
+ if (primaryExtension != "")
+ typeString = this.dialogElement("strings").getFormattedString("fileType", [primaryExtension.toUpperCase()]);
+ // 3. If we can't even do that, just give up and show the MIME type.
+ else
+ typeString = mimeInfo.MIMEType;
+ }
+ // When the length is unknown, contentLength would be -1
+ if (this.mLauncher.contentLength >= 0) {
+ let [size, unit] = DownloadUtils.
+ convertByteUnits(this.mLauncher.contentLength);
+ type.value = this.dialogElement("strings")
+ .getFormattedString("orderedFileSizeWithType",
+ [typeString, size, unit]);
+ }
+ else {
+ type.value = typeString;
+ }
+ },
+
+ // Returns true if opening the default application makes sense.
+ openWithDefaultOK: function() {
+ // The checking is different on Windows...
+ if (AppConstants.platform == "win") {
+ // Windows presents some special cases.
+ // We need to prevent use of "system default" when the file is
+ // executable (so the user doesn't launch nasty programs downloaded
+ // from the web), and, enable use of "system default" if it isn't
+ // executable (because we will prompt the user for the default app
+ // in that case).
+
+ // Default is Ok if the file isn't executable (and vice-versa).
+ return !this.mLauncher.targetFileIsExecutable;
+ }
+ // On other platforms, default is Ok if there is a default app.
+ // Note that nsIMIMEInfo providers need to ensure that this holds true
+ // on each platform.
+ return this.mLauncher.MIMEInfo.hasDefaultHandler;
+ },
+
+ // Set "default" application description field.
+ initDefaultApp: function() {
+ // Use description, if we can get one.
+ var desc = this.mLauncher.MIMEInfo.defaultDescription;
+ if (desc) {
+ var defaultApp = this.dialogElement("strings").getFormattedString("defaultApp", [desc]);
+ this.dialogElement("defaultHandler").label = defaultApp;
+ }
+ else {
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "1");
+ // Hide the default handler item too, in case the user picks a
+ // custom handler at a later date which triggers the menulist to show.
+ this.dialogElement("defaultHandler").hidden = true;
+ }
+ },
+
+ // getPath:
+ getPath: function (aFile) {
+ if (AppConstants.platform == "macosx") {
+ return aFile.leafName || aFile.path;
+ }
+ return aFile.path;
+ },
+
+ // initAppAndSaveToDiskValues:
+ initAppAndSaveToDiskValues: function() {
+ var modeGroup = this.dialogElement("mode");
+
+ // We don't let users open .exe files or random binary data directly
+ // from the browser at the moment because of security concerns.
+ var openWithDefaultOK = this.openWithDefaultOK();
+ var mimeType = this.mLauncher.MIMEInfo.MIMEType;
+ if (this.mLauncher.targetFileIsExecutable || (
+ (mimeType == "application/octet-stream" ||
+ mimeType == "application/x-msdownload") &&
+ !openWithDefaultOK)) {
+ this.dialogElement("open").disabled = true;
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.disabled = true;
+ openHandler.selectedItem = null;
+ modeGroup.selectedItem = this.dialogElement("save");
+ return;
+ }
+
+ // Fill in helper app info, if there is any.
+ try {
+ this.chosenApp =
+ this.mLauncher.MIMEInfo.preferredApplicationHandler
+ .QueryInterface(Components.interfaces.nsILocalHandlerApp);
+ } catch (e) {
+ this.chosenApp = null;
+ }
+ // Initialize "default application" field.
+ this.initDefaultApp();
+
+ var otherHandler = this.dialogElement("otherHandler");
+
+ // Fill application name textbox.
+ if (this.chosenApp && this.chosenApp.executable &&
+ this.chosenApp.executable.path) {
+ otherHandler.setAttribute("path",
+ this.getPath(this.chosenApp.executable));
+
+ otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
+ otherHandler.hidden = false;
+ }
+
+ var openHandler = this.dialogElement("openHandler");
+ openHandler.selectedIndex = 0;
+ var defaultOpenHandler = this.dialogElement("defaultHandler");
+
+ if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useSystemDefault) {
+ // Open (using system default).
+ modeGroup.selectedItem = this.dialogElement("open");
+ } else if (this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.useHelperApp) {
+ // Open with given helper app.
+ modeGroup.selectedItem = this.dialogElement("open");
+ openHandler.selectedItem = (otherHandler && !otherHandler.hidden) ?
+ otherHandler : defaultOpenHandler;
+ } else {
+ // Save to disk.
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+
+ // If we don't have a "default app" then disable that choice.
+ if (!openWithDefaultOK) {
+ var isSelected = defaultOpenHandler.selected;
+
+ // Disable that choice.
+ defaultOpenHandler.hidden = true;
+ // If that's the default, then switch to "save to disk."
+ if (isSelected) {
+ openHandler.selectedIndex = 1;
+ modeGroup.selectedItem = this.dialogElement("save");
+ }
+ }
+
+ otherHandler.nextSibling.hidden = otherHandler.nextSibling.nextSibling.hidden = false;
+ this.updateOKButton();
+ },
+
+ // Returns the user-selected application
+ helperAppChoice: function() {
+ return this.chosenApp;
+ },
+
+ get saveToDisk() {
+ return this.dialogElement("save").selected;
+ },
+
+ get useOtherHandler() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 1;
+ },
+
+ get useSystemDefault() {
+ return this.dialogElement("open").selected && this.dialogElement("openHandler").selectedIndex == 0;
+ },
+
+ toggleRememberChoice: function (aCheckbox) {
+ this.dialogElement("settingsChange").hidden = !aCheckbox.checked;
+ this.mDialog.sizeToContent();
+ },
+
+ openHandlerCommand: function () {
+ var openHandler = this.dialogElement("openHandler");
+ if (openHandler.selectedItem.id == "choose")
+ this.chooseApp();
+ else
+ openHandler.setAttribute("lastSelectedItemID", openHandler.selectedItem.id);
+ },
+
+ updateOKButton: function() {
+ var ok = false;
+ if (this.dialogElement("save").selected) {
+ // This is always OK.
+ ok = true;
+ }
+ else if (this.dialogElement("open").selected) {
+ switch (this.dialogElement("openHandler").selectedIndex) {
+ case 0:
+ // No app need be specified in this case.
+ ok = true;
+ break;
+ case 1:
+ // only enable the OK button if we have a default app to use or if
+ // the user chose an app....
+ ok = this.chosenApp || /\S/.test(this.dialogElement("otherHandler").getAttribute("path"));
+ break;
+ }
+ }
+
+ // Enable Ok button if ok to press.
+ this.mDialog.document.documentElement.getButton("accept").disabled = !ok;
+ },
+
+ // Returns true iff the user-specified helper app has been modified.
+ appChanged: function() {
+ return this.helperAppChoice() != this.mLauncher.MIMEInfo.preferredApplicationHandler;
+ },
+
+ updateMIMEInfo: function() {
+ // Don't update mime type preferences when the preferred action is set to
+ // the internal handler -- this dialog is the result of the handler fallback
+ // (e.g. Content-Disposition was set as attachment)
+ var discardUpdate = this.mLauncher.MIMEInfo.preferredAction == this.nsIMIMEInfo.handleInternally &&
+ !this.dialogElement("rememberChoice").checked;
+
+ var needUpdate = false;
+ // If current selection differs from what's in the mime info object,
+ // then we need to update.
+ if (this.saveToDisk) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.saveToDisk;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.saveToDisk;
+ }
+ else if (this.useSystemDefault) {
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useSystemDefault;
+ if (needUpdate)
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useSystemDefault;
+ }
+ else {
+ // For "open with", we need to check both preferred action and whether the user chose
+ // a new app.
+ needUpdate = this.mLauncher.MIMEInfo.preferredAction != this.nsIMIMEInfo.useHelperApp || this.appChanged();
+ if (needUpdate) {
+ this.mLauncher.MIMEInfo.preferredAction = this.nsIMIMEInfo.useHelperApp;
+ // App may have changed - Update application
+ var app = this.helperAppChoice();
+ this.mLauncher.MIMEInfo.preferredApplicationHandler = app;
+ }
+ }
+ // We will also need to update if the "always ask" flag has changed.
+ needUpdate = needUpdate || this.mLauncher.MIMEInfo.alwaysAskBeforeHandling != (!this.dialogElement("rememberChoice").checked);
+
+ // One last special case: If the input "always ask" flag was false, then we always
+ // update. In that case we are displaying the helper app dialog for the first
+ // time for this mime type and we need to store the user's action in the mimeTypes.rdf
+ // data source (whether that action has changed or not; if it didn't change, then we need
+ // to store the "always ask" flag so the helper app dialog will or won't display
+ // next time, per the user's selection).
+ needUpdate = needUpdate || !this.mLauncher.MIMEInfo.alwaysAskBeforeHandling;
+
+ // Make sure mime info has updated setting for the "always ask" flag.
+ this.mLauncher.MIMEInfo.alwaysAskBeforeHandling = !this.dialogElement("rememberChoice").checked;
+
+ return needUpdate && !discardUpdate;
+ },
+
+ // See if the user changed things, and if so, update the
+ // mimeTypes.rdf entry for this mime type.
+ updateHelperAppPref: function() {
+ var handlerInfo = this.mLauncher.MIMEInfo;
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"].getService(Ci.nsIHandlerService);
+ hs.store(handlerInfo);
+ },
+
+ // onOK:
+ onOK: function() {
+ // Verify typed app path, if necessary.
+ if (this.useOtherHandler) {
+ var helperApp = this.helperAppChoice();
+ if (!helperApp || !helperApp.executable ||
+ !helperApp.executable.exists()) {
+ // Show alert and try again.
+ var bundle = this.dialogElement("strings");
+ var msg = bundle.getFormattedString("badApp", [this.dialogElement("otherHandler").getAttribute("path")]);
+ Services.prompt.alert(this.mDialog, bundle.getString("badApp.title"), msg);
+
+ // Disable the OK button.
+ this.mDialog.document.documentElement.getButton("accept").disabled = true;
+ this.dialogElement("mode").focus();
+
+ // Clear chosen application.
+ this.chosenApp = null;
+
+ // Leave dialog up.
+ return false;
+ }
+ }
+
+ // Remove our web progress listener (a progress dialog will be
+ // taking over).
+ this.mLauncher.setWebProgressListener(null);
+
+ // saveToDisk and launchWithApplication can return errors in
+ // certain circumstances (e.g. The user clicks cancel in the
+ // "Save to Disk" dialog. In those cases, we don't want to
+ // update the helper application preferences in the RDF file.
+ try {
+ var needUpdate = this.updateMIMEInfo();
+
+ if (this.dialogElement("save").selected) {
+ // If we're using a default download location, create a path
+ // for the file to be saved to to pass to |saveToDisk| - otherwise
+ // we must ask the user to pick a save name.
+
+ /*
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"].getService(Components.interfaces.nsIPrefBranch);
+ var targetFile = null;
+ try {
+ targetFile = prefs.getComplexValue("browser.download.defaultFolder",
+ Components.interfaces.nsILocalFile);
+ var leafName = this.dialogElement("location").getAttribute("realname");
+ // Ensure that we don't overwrite any existing files here.
+ targetFile = this.validateLeafName(targetFile, leafName, null);
+ }
+ catch(e) { }
+
+ this.mLauncher.saveToDisk(targetFile, false);
+ */
+
+ // see @notify
+ // we cannot use opener's setTimeout, see bug 420405
+ this._saveToDiskTimer = Components.classes["@mozilla.org/timer;1"]
+ .createInstance(nsITimer);
+ this._saveToDiskTimer.initWithCallback(this, 0,
+ nsITimer.TYPE_ONE_SHOT);
+ }
+ else
+ this.mLauncher.launchWithApplication(null, false);
+
+ // Update user pref for this mime type (if necessary). We do not
+ // store anything in the mime type preferences for the ambiguous
+ // type application/octet-stream. We do NOT do this for
+ // application/x-msdownload since we want users to be able to
+ // autodownload these to disk.
+ if (needUpdate && this.mLauncher.MIMEInfo.MIMEType != "application/octet-stream")
+ this.updateHelperAppPref();
+ } catch(e) { }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // onCancel:
+ onCancel: function() {
+ // Remove our web progress listener.
+ this.mLauncher.setWebProgressListener(null);
+
+ // Cancel app launcher.
+ try {
+ this.mLauncher.cancel(Components.results.NS_BINDING_ABORTED);
+ } catch(exception) {
+ }
+
+ // Unhook dialog from this object.
+ this.mDialog.dialog = null;
+
+ // Close up dialog by returning true.
+ return true;
+ },
+
+ // dialogElement: Convenience.
+ dialogElement: function(id) {
+ return this.mDialog.document.getElementById(id);
+ },
+
+ // Retrieve the pretty description from the file
+ getFileDisplayName: function getFileDisplayName(file)
+ {
+ if (AppConstants.platform == "win") {
+ if (file instanceof Components.interfaces.nsILocalFileWin) {
+ try {
+ return file.getVersionInfoField("FileDescription");
+ } catch (e) {}
+ }
+ } else if (AppConstants.platform == "macosx") {
+ if (file instanceof Components.interfaces.nsILocalFileMac) {
+ try {
+ return file.bundleDisplayName;
+ } catch (e) {}
+ }
+ }
+ return file.leafName;
+ },
+
+ finishChooseApp: function() {
+ if (this.chosenApp) {
+ // Show the "handler" menulist since we have a (user-specified)
+ // application now.
+ this.dialogElement("modeDeck").setAttribute("selectedIndex", "0");
+
+ // Update dialog.
+ var otherHandler = this.dialogElement("otherHandler");
+ otherHandler.removeAttribute("hidden");
+ otherHandler.setAttribute("path", this.getPath(this.chosenApp.executable));
+ if (AppConstants.platform == "win")
+ otherHandler.label = this.getFileDisplayName(this.chosenApp.executable);
+ else
+ otherHandler.label = this.chosenApp.name;
+ this.dialogElement("openHandler").selectedIndex = 1;
+ this.dialogElement("openHandler").setAttribute("lastSelectedItemID", "otherHandler");
+
+ this.dialogElement("mode").selectedItem = this.dialogElement("open");
+ }
+ else {
+ var openHandler = this.dialogElement("openHandler");
+ var lastSelectedID = openHandler.getAttribute("lastSelectedItemID");
+ if (!lastSelectedID)
+ lastSelectedID = "defaultHandler";
+ openHandler.selectedItem = this.dialogElement(lastSelectedID);
+ }
+ },
+ // chooseApp: Open file picker and prompt user for application.
+ chooseApp: function() {
+ if (AppConstants.platform == "win") {
+ // Protect against the lack of an extension
+ var fileExtension = "";
+ try {
+ fileExtension = this.mLauncher.MIMEInfo.primaryExtension;
+ } catch(ex) {
+ }
+
+ // Try to use the pretty description of the type, if one is available.
+ var typeString = this.mLauncher.MIMEInfo.description;
+
+ if (!typeString) {
+ // If there is none, use the extension to
+ // identify the file, e.g. "ZIP file"
+ if (fileExtension) {
+ typeString =
+ this.dialogElement("strings").
+ getFormattedString("fileType", [fileExtension.toUpperCase()]);
+ } else {
+ // If we can't even do that, just give up and show the MIME type.
+ typeString = this.mLauncher.MIMEInfo.MIMEType;
+ }
+ }
+
+ var params = {};
+ params.title =
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle");
+ params.description = typeString;
+ params.filename = this.mLauncher.suggestedFileName;
+ params.mimeInfo = this.mLauncher.MIMEInfo;
+ params.handlerApp = null;
+
+ this.mDialog.openDialog("chrome://global/content/appPicker.xul", null,
+ "chrome,modal,centerscreen,titlebar,dialog=yes",
+ params);
+
+ if (params.handlerApp &&
+ params.handlerApp.executable &&
+ params.handlerApp.executable.isFile()) {
+ // Remember the file they chose to run.
+ this.chosenApp = params.handlerApp;
+ }
+ }
+ else {
+#if MOZ_WIDGET_GTK == 3
+ var nsIApplicationChooser = Components.interfaces.nsIApplicationChooser;
+ var appChooser = Components.classes["@mozilla.org/applicationchooser;1"]
+ .createInstance(nsIApplicationChooser);
+ appChooser.init(this.mDialog, this.dialogElement("strings").getString("chooseAppFilePickerTitle"));
+ var contentTypeDialogObj = this;
+ let appChooserCallback = function appChooserCallback_done(aResult) {
+ if (aResult) {
+ contentTypeDialogObj.chosenApp = aResult.QueryInterface(Components.interfaces.nsILocalHandlerApp);
+ }
+ contentTypeDialogObj.finishChooseApp();
+ };
+ appChooser.open(this.mLauncher.MIMEInfo.MIMEType, appChooserCallback);
+ // The finishChooseApp is called from appChooserCallback
+ return;
+#else
+ var nsIFilePicker = Components.interfaces.nsIFilePicker;
+ var fp = Components.classes["@mozilla.org/filepicker;1"]
+ .createInstance(nsIFilePicker);
+ fp.init(this.mDialog,
+ this.dialogElement("strings").getString("chooseAppFilePickerTitle"),
+ nsIFilePicker.modeOpen);
+
+ fp.appendFilters(nsIFilePicker.filterApps);
+
+ if (fp.show() == nsIFilePicker.returnOK && fp.file) {
+ // Remember the file they chose to run.
+ var localHandlerApp =
+ Components.classes["@mozilla.org/uriloader/local-handler-app;1"].
+ createInstance(Components.interfaces.nsILocalHandlerApp);
+ localHandlerApp.executable = fp.file;
+ this.chosenApp = localHandlerApp;
+ }
+#endif // MOZ_WIDGET_GTK == 3
+ }
+ this.finishChooseApp();
+ },
+
+ // Turn this on to get debugging messages.
+ debug: false,
+
+ // Dump text (if debug is on).
+ dump: function( text ) {
+ if ( this.debug ) {
+ dump( text );
+ }
+ },
+
+ // dumpObj:
+ dumpObj: function( spec ) {
+ var val = "<undefined>";
+ try {
+ val = eval( "this."+spec ).toString();
+ } catch( exception ) {
+ }
+ this.dump( spec + "=" + val + "\n" );
+ },
+
+ // dumpObjectProperties
+ dumpObjectProperties: function( desc, obj ) {
+ for( prop in obj ) {
+ this.dump( desc + "." + prop + "=" );
+ var val = "<undefined>";
+ try {
+ val = obj[ prop ];
+ } catch ( exception ) {
+ }
+ this.dump( val + "\n" );
+ }
+ }
+}
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([nsUnknownContentTypeDialog]);
diff --git a/toolkit/mozapps/downloads/nsHelperAppDlg.manifest b/toolkit/mozapps/downloads/nsHelperAppDlg.manifest
new file mode 100644
index 000000000..8824b45a2
--- /dev/null
+++ b/toolkit/mozapps/downloads/nsHelperAppDlg.manifest
@@ -0,0 +1,2 @@
+component {F68578EB-6EC2-4169-AE19-8C6243F0ABE1} nsHelperAppDlg.js
+contract @mozilla.org/helperapplauncherdialog;1 {F68578EB-6EC2-4169-AE19-8C6243F0ABE1}
diff --git a/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js b/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/downloads/tests/chrome/chrome.ini b/toolkit/mozapps/downloads/tests/chrome/chrome.ini
new file mode 100644
index 000000000..b5a29cb45
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/chrome.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+skip-if = os == 'android'
+support-files =
+ unknownContentType_dialog_layout_data.pif
+ unknownContentType_dialog_layout_data.pif^headers^
+ unknownContentType_dialog_layout_data.txt
+ unknownContentType_dialog_layout_data.txt^headers^
+
+[test_unknownContentType_delayedbutton.xul]
+[test_unknownContentType_dialog_layout.xul]
diff --git a/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul
new file mode 100644
index 000000000..9bbec0f92
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_delayedbutton.xul
@@ -0,0 +1,117 @@
+<?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/. -->
+<!--
+ * The unknownContentType popup can have two different layouts depending on
+ * whether a helper application can be selected or not.
+ * This tests that both layouts have correct collapsed elements.
+-->
+
+<window title="Unknown Content Type Dialog Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="doTest()">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script type="application/javascript"><![CDATA[
+ const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+ Cu.import("resource://gre/modules/Services.jsm");
+ Cu.import("resource://gre/modules/Task.jsm");
+ Cu.import("resource://gre/modules/Promise.jsm");
+
+ const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul";
+ const LOAD_URI = "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt";
+
+ const DIALOG_DELAY = Services.prefs.getIntPref("security.dialog_enable_delay") + 200;
+
+ let UCTObserver = {
+ opened: Promise.defer(),
+ closed: Promise.defer(),
+
+ observe: function(aSubject, aTopic, aData) {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+
+ switch (aTopic) {
+ case "domwindowopened":
+ win.addEventListener("load", function onLoad(event) {
+ win.removeEventListener("load", onLoad, false);
+
+ // Let the dialog initialize
+ SimpleTest.executeSoon(function() {
+ UCTObserver.opened.resolve(win);
+ });
+ }, false);
+ break;
+
+ case "domwindowclosed":
+ if (win.location == UCT_URI) {
+ this.closed.resolve();
+ }
+ break;
+ }
+ }
+ };
+
+ Services.ww.registerNotification(UCTObserver);
+ SimpleTest.waitForExplicitFinish();
+ SimpleTest.requestFlakyTimeout("This test is testing a timing-based feature, so it really needs to wait a certain amount of time to verify that the feature worked.");
+
+ function waitDelay(delay) {
+ return new Promise((resolve, reject) => {
+ window.setTimeout(resolve, delay);
+ });
+ }
+
+ function doTest() {
+ Task.spawn(function test_aboutCrashed() {
+ let frame = document.getElementById("testframe");
+ frame.setAttribute("src", LOAD_URI);
+
+ let uctWindow = yield UCTObserver.opened.promise;
+ let ok = uctWindow.document.documentElement.getButton("accept");
+
+ SimpleTest.is(ok.disabled, true, "button started disabled");
+
+ yield waitDelay(DIALOG_DELAY);
+
+ SimpleTest.is(ok.disabled, false, "button was enabled");
+
+ focusOutOfDialog = SimpleTest.promiseFocus(window);
+ window.focus();
+ yield focusOutOfDialog;
+
+ SimpleTest.is(ok.disabled, true, "button was disabled");
+
+ focusOnDialog = SimpleTest.promiseFocus(uctWindow);
+ uctWindow.focus();
+ yield focusOnDialog;
+
+ SimpleTest.is(ok.disabled, true, "button remained disabled");
+
+ yield waitDelay(DIALOG_DELAY);
+ SimpleTest.is(ok.disabled, false, "button re-enabled after delay");
+
+ uctWindow.document.documentElement.cancelDialog();
+ yield UCTObserver.closed.promise;
+
+ Services.ww.unregisterNotification(UCTObserver);
+ uctWindow = null;
+ UCTObserver = null;
+ SimpleTest.finish();
+ });
+ }
+ ]]></script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <iframe xmlns="http://www.w3.org/1999/xhtml"
+ id="testframe">
+ </iframe>
+</window>
diff --git a/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul
new file mode 100644
index 000000000..1210b908d
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/test_unknownContentType_dialog_layout.xul
@@ -0,0 +1,108 @@
+<?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/. -->
+<!--
+ * The unknownContentType popup can have two different layouts depending on
+ * whether a helper application can be selected or not.
+ * This tests that both layouts have correct collapsed elements.
+-->
+
+<window title="Unknown Content Type Dialog Test"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init()">
+
+ <script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+
+ <script type="application/javascript">
+ <![CDATA[
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+const UCT_URI = "chrome://mozapps/content/downloads/unknownContentType.xul";
+
+let testIndex = -1;
+let tests = [
+ { // This URL will trigger the simple UI, where only the Save an Cancel buttons are available
+ url: "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif",
+ elements: {
+ basicBox: { collapsed: false },
+ normalBox: { collapsed: true }
+ }
+ },
+ { // This URL will trigger the full UI
+ url: "http://mochi.test:8888/chrome/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt",
+ elements: {
+ basicBox: { collapsed: true },
+ normalBox: { collapsed: false }
+ }
+ }
+];
+
+let ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+
+SimpleTest.waitForExplicitFinish();
+
+let windowObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+
+ if (aTopic == "domwindowclosed") {
+ if (win.location == UCT_URI)
+ loadNextTest();
+ return;
+ }
+
+ // domwindowopened
+ win.addEventListener("load", function onLoad(event) {
+ win.removeEventListener("load", onLoad, false);
+
+ // Let the dialog initialize
+ SimpleTest.executeSoon(function() {
+ checkWindow(win);
+ });
+ }, false);
+ }
+};
+
+function init() {
+ ww.registerNotification(windowObserver);
+ loadNextTest();
+}
+
+function loadNextTest() {
+ if (!tests[++testIndex]) {
+ ww.unregisterNotification(windowObserver);
+ SimpleTest.finish();
+ return;
+ }
+ let frame = document.getElementById("testframe");
+ frame.setAttribute("src", tests[testIndex].url);
+}
+
+function checkWindow(win) {
+ for (let [id, props] of Object.entries(tests[testIndex].elements)) {
+ let elem = win.dialog.dialogElement(id);
+ for (let [prop, value] of Object.entries(props)) {
+ is(elem[prop], value,
+ "Element with id " + id + " has property " +
+ prop + " set to " + value);
+ }
+ }
+ win.document.documentElement.cancelDialog();
+}
+
+ ]]>
+ </script>
+
+ <body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display:none;"></div>
+ <pre id="test"></pre>
+ </body>
+
+ <iframe xmlns="http://www.w3.org/1999/xhtml"
+ id="testframe">
+ </iframe>
+</window>
diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif
new file mode 100644
index 000000000..9353d1312
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif
@@ -0,0 +1 @@
+Dummy content for unknownContentType_dialog_layout_data.pif
diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^
new file mode 100644
index 000000000..09b22facc
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.pif^headers^
@@ -0,0 +1 @@
+Content-Type: application/octet-stream
diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt
new file mode 100644
index 000000000..77e719559
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt
@@ -0,0 +1 @@
+Dummy content for unknownContentType_dialog_layout_data.txt
diff --git a/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^
new file mode 100644
index 000000000..2a3c472e2
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/chrome/unknownContentType_dialog_layout_data.txt^headers^
@@ -0,0 +1,2 @@
+Content-Type: text/plain
+Content-Disposition: attachment
diff --git a/toolkit/mozapps/downloads/tests/moz.build b/toolkit/mozapps/downloads/tests/moz.build
new file mode 100644
index 000000000..a4b1efb9a
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/moz.build
@@ -0,0 +1,8 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPCSHELL_TESTS_MANIFESTS += ['unit/xpcshell.ini']
+MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
diff --git a/toolkit/mozapps/downloads/tests/unit/.eslintrc.js b/toolkit/mozapps/downloads/tests/unit/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/downloads/tests/unit/head_downloads.js b/toolkit/mozapps/downloads/tests/unit/head_downloads.js
new file mode 100644
index 000000000..4f199e5cf
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/head_downloads.js
@@ -0,0 +1,5 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+do_register_cleanup(function() {
+ Services.obs.notifyObservers(null, "quit-application", null);
+});
diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js
new file mode 100644
index 000000000..77249169d
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadPaths.js
@@ -0,0 +1,131 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ts=2 et sw=2 tw=80 filetype=javascript: */
+/* ***** BEGIN LICENSE BLOCK *****
+ *
+ * Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ *
+ * ***** END LICENSE BLOCK ***** */
+
+/**
+ * Tests for the "DownloadPaths.jsm" JavaScript module.
+ */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://gre/modules/DownloadPaths.jsm");
+
+/**
+ * Provides a temporary save directory.
+ *
+ * @returns nsIFile pointing to the new or existing directory.
+ */
+function createTemporarySaveDirectory()
+{
+ var saveDir = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).get("TmpD", Ci.nsIFile);
+ saveDir.append("testsavedir");
+ if (!saveDir.exists()) {
+ saveDir.create(Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ }
+ return saveDir;
+}
+
+function testSplitBaseNameAndExtension(aLeafName, [aBase, aExt])
+{
+ var [base, ext] = DownloadPaths.splitBaseNameAndExtension(aLeafName);
+ do_check_eq(base, aBase);
+ do_check_eq(ext, aExt);
+
+ // If we modify the base name and concatenate it with the extension again,
+ // another roundtrip through the function should give a consistent result.
+ // The only exception is when we introduce an extension in a file name that
+ // didn't have one or that ended with one of the special cases like ".gz". If
+ // we avoid using a dot and we introduce at least another special character,
+ // the results are always consistent.
+ [base, ext] = DownloadPaths.splitBaseNameAndExtension("(" + base + ")" + ext);
+ do_check_eq(base, "(" + aBase + ")");
+ do_check_eq(ext, aExt);
+}
+
+function testCreateNiceUniqueFile(aTempFile, aExpectedLeafName)
+{
+ var createdFile = DownloadPaths.createNiceUniqueFile(aTempFile);
+ do_check_eq(createdFile.leafName, aExpectedLeafName);
+}
+
+function run_test()
+{
+ // Usual file names.
+ testSplitBaseNameAndExtension("base", ["base", ""]);
+ testSplitBaseNameAndExtension("base.ext", ["base", ".ext"]);
+ testSplitBaseNameAndExtension("base.application", ["base", ".application"]);
+ testSplitBaseNameAndExtension("base.x.Z", ["base", ".x.Z"]);
+ testSplitBaseNameAndExtension("base.ext.Z", ["base", ".ext.Z"]);
+ testSplitBaseNameAndExtension("base.ext.gz", ["base", ".ext.gz"]);
+ testSplitBaseNameAndExtension("base.ext.Bz2", ["base", ".ext.Bz2"]);
+ testSplitBaseNameAndExtension("base..ext", ["base.", ".ext"]);
+ testSplitBaseNameAndExtension("base..Z", ["base.", ".Z"]);
+ testSplitBaseNameAndExtension("base. .Z", ["base. ", ".Z"]);
+ testSplitBaseNameAndExtension("base.base.Bz2", ["base.base", ".Bz2"]);
+ testSplitBaseNameAndExtension("base .ext", ["base ", ".ext"]);
+
+ // Corner cases. A name ending with a dot technically has no extension, but
+ // we consider the ending dot separately from the base name so that modifying
+ // the latter never results in an extension being introduced accidentally.
+ // Names beginning with a dot are hidden files on Unix-like platforms and if
+ // their name doesn't contain another dot they should have no extension, but
+ // on Windows the whole name is considered as an extension.
+ testSplitBaseNameAndExtension("base.", ["base", "."]);
+ testSplitBaseNameAndExtension(".ext", ["", ".ext"]);
+
+ // Unusual file names (not recommended as input to the function).
+ testSplitBaseNameAndExtension("base. ", ["base", ". "]);
+ testSplitBaseNameAndExtension("base ", ["base ", ""]);
+ testSplitBaseNameAndExtension("", ["", ""]);
+ testSplitBaseNameAndExtension(" ", [" ", ""]);
+ testSplitBaseNameAndExtension(" . ", [" ", ". "]);
+ testSplitBaseNameAndExtension(" .. ", [" .", ". "]);
+ testSplitBaseNameAndExtension(" .ext", [" ", ".ext"]);
+ testSplitBaseNameAndExtension(" .ext. ", [" .ext", ". "]);
+ testSplitBaseNameAndExtension(" .ext.gz ", [" .ext", ".gz "]);
+
+ var destDir = createTemporarySaveDirectory();
+ try {
+ // Single extension.
+ var tempFile = destDir.clone();
+ tempFile.append("test.txt");
+ testCreateNiceUniqueFile(tempFile, "test.txt");
+ testCreateNiceUniqueFile(tempFile, "test(1).txt");
+ testCreateNiceUniqueFile(tempFile, "test(2).txt");
+
+ // Double extension.
+ tempFile.leafName = "test.tar.gz";
+ testCreateNiceUniqueFile(tempFile, "test.tar.gz");
+ testCreateNiceUniqueFile(tempFile, "test(1).tar.gz");
+ testCreateNiceUniqueFile(tempFile, "test(2).tar.gz");
+
+ // Test automatic shortening of long file names. We don't know exactly how
+ // many characters are removed, because it depends on the name of the folder
+ // where the file is located.
+ tempFile.leafName = new Array(256).join("T") + ".txt";
+ var newFile = DownloadPaths.createNiceUniqueFile(tempFile);
+ do_check_true(newFile.leafName.length < tempFile.leafName.length);
+ do_check_eq(newFile.leafName.slice(-4), ".txt");
+
+ // Creating a valid file name from an invalid one is not always possible.
+ tempFile.append("file-under-long-directory.txt");
+ try {
+ DownloadPaths.createNiceUniqueFile(tempFile);
+ do_throw("Exception expected with a long parent directory name.")
+ } catch (e) {
+ // An exception is expected, but we don't know which one exactly.
+ }
+ } finally {
+ // Clean up the temporary directory.
+ destDir.remove(true);
+ }
+}
diff --git a/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
new file mode 100644
index 000000000..11e7776a7
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/test_DownloadUtils.js
@@ -0,0 +1,237 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+
+const gDecimalSymbol = Number(5.4).toLocaleString().match(/\D/);
+function _(str) {
+ return str.replace(/\./g, gDecimalSymbol);
+}
+
+function testConvertByteUnits(aBytes, aValue, aUnit)
+{
+ let [value, unit] = DownloadUtils.convertByteUnits(aBytes);
+ do_check_eq(value, aValue);
+ do_check_eq(unit, aUnit);
+}
+
+function testTransferTotal(aCurrBytes, aMaxBytes, aTransfer)
+{
+ let transfer = DownloadUtils.getTransferTotal(aCurrBytes, aMaxBytes);
+ do_check_eq(transfer, aTransfer);
+}
+
+// Get the em-dash character because typing it directly here doesn't work :(
+var gDash = DownloadUtils.getDownloadStatus(0)[0].match(/remaining (.) 0 bytes/)[1];
+
+var gVals = [0, 100, 2345, 55555, 982341, 23194134, 1482, 58, 9921949201, 13498132, Infinity];
+
+function testStatus(aFunc, aCurr, aMore, aRate, aTest)
+{
+ dump("Status Test: " + [aCurr, aMore, aRate, aTest] + "\n");
+ let curr = gVals[aCurr];
+ let max = curr + gVals[aMore];
+ let speed = gVals[aRate];
+
+ let [status, last] = aFunc(curr, max, speed);
+
+ if (0) {
+ dump("testStatus(" + aCurr + ", " + aMore + ", " + aRate + ", [\"" +
+ status.replace(gDash, "--") + "\", " + last.toFixed(3) + "]);\n");
+ }
+
+ // Make sure the status text matches
+ do_check_eq(status, _(aTest[0].replace(/--/, gDash)));
+
+ // Make sure the lastSeconds matches
+ if (last == Infinity)
+ do_check_eq(last, aTest[1]);
+ else
+ do_check_true(Math.abs(last - aTest[1]) < .1);
+}
+
+function testURI(aURI, aDisp, aHost)
+{
+ dump("URI Test: " + [aURI, aDisp, aHost] + "\n");
+
+ let [disp, host] = DownloadUtils.getURIHost(aURI);
+
+ // Make sure we have the right display host and full host
+ do_check_eq(disp, aDisp);
+ do_check_eq(host, aHost);
+}
+
+
+function testGetReadableDates(aDate, aCompactValue)
+{
+ const now = new Date(2000, 11, 31, 11, 59, 59);
+
+ let [dateCompact] = DownloadUtils.getReadableDates(aDate, now);
+ do_check_eq(dateCompact, aCompactValue);
+}
+
+function testAllGetReadableDates()
+{
+ // This test cannot depend on the current date and time, or the date format.
+ // It depends on being run with the English localization, however.
+ const today_11_30 = new Date(2000, 11, 31, 11, 30, 15);
+ const today_12_30 = new Date(2000, 11, 31, 12, 30, 15);
+ const yesterday_11_30 = new Date(2000, 11, 30, 11, 30, 15);
+ const yesterday_12_30 = new Date(2000, 11, 30, 12, 30, 15);
+ const twodaysago = new Date(2000, 11, 29, 11, 30, 15);
+ const sixdaysago = new Date(2000, 11, 25, 11, 30, 15);
+ const sevendaysago = new Date(2000, 11, 24, 11, 30, 15);
+
+ // TODO: Remove Intl fallback when bug 1215247 is fixed.
+ const locale = typeof Intl === "undefined"
+ ? undefined
+ : Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIXULChromeRegistry)
+ .getSelectedLocale("global", true);
+
+ let dts = Components.classes["@mozilla.org/intl/scriptabledateformat;1"].
+ getService(Components.interfaces.nsIScriptableDateFormat);
+
+ testGetReadableDates(today_11_30, dts.FormatTime("", dts.timeFormatNoSeconds,
+ 11, 30, 0));
+ testGetReadableDates(today_12_30, dts.FormatTime("", dts.timeFormatNoSeconds,
+ 12, 30, 0));
+ testGetReadableDates(yesterday_11_30, "Yesterday");
+ testGetReadableDates(yesterday_12_30, "Yesterday");
+ testGetReadableDates(twodaysago,
+ typeof Intl === "undefined"
+ ? twodaysago.toLocaleFormat("%A")
+ : twodaysago.toLocaleDateString(locale, { weekday: "long" }));
+ testGetReadableDates(sixdaysago,
+ typeof Intl === "undefined"
+ ? sixdaysago.toLocaleFormat("%A")
+ : sixdaysago.toLocaleDateString(locale, { weekday: "long" }));
+ testGetReadableDates(sevendaysago,
+ (typeof Intl === "undefined"
+ ? sevendaysago.toLocaleFormat("%B")
+ : sevendaysago.toLocaleDateString(locale, { month: "long" })) + " " +
+ sevendaysago.getDate().toString().padStart(2, "0"));
+
+ let [, dateTimeFull] = DownloadUtils.getReadableDates(today_11_30);
+ do_check_eq(dateTimeFull, dts.FormatDateTime("", dts.dateFormatLong,
+ dts.timeFormatNoSeconds,
+ 2000, 12, 31, 11, 30, 0));
+}
+
+function run_test()
+{
+ testConvertByteUnits(-1, "-1", "bytes");
+ testConvertByteUnits(1, _("1"), "bytes");
+ testConvertByteUnits(42, _("42"), "bytes");
+ testConvertByteUnits(123, _("123"), "bytes");
+ testConvertByteUnits(1024, _("1.0"), "KB");
+ testConvertByteUnits(8888, _("8.7"), "KB");
+ testConvertByteUnits(59283, _("57.9"), "KB");
+ testConvertByteUnits(640000, _("625"), "KB");
+ testConvertByteUnits(1048576, _("1.0"), "MB");
+ testConvertByteUnits(307232768, _("293"), "MB");
+ testConvertByteUnits(1073741824, _("1.0"), "GB");
+
+ testTransferTotal(1, 1, _("1 of 1 bytes"));
+ testTransferTotal(234, 4924, _("234 bytes of 4.8 KB"));
+ testTransferTotal(94923, 233923, _("92.7 of 228 KB"));
+ testTransferTotal(4924, 94923, _("4.8 of 92.7 KB"));
+ testTransferTotal(2342, 294960345, _("2.3 KB of 281 MB"));
+ testTransferTotal(234, undefined, _("234 bytes"));
+ testTransferTotal(4889023, undefined, _("4.7 MB"));
+
+ if (0) {
+ // Help find some interesting test cases
+ let r = () => Math.floor(Math.random() * 10);
+ for (let i = 0; i < 100; i++) {
+ testStatus(r(), r(), r());
+ }
+ }
+
+ // First, test with rates, via getDownloadStatus...
+ let statusFunc = DownloadUtils.getDownloadStatus.bind(DownloadUtils);
+
+ testStatus(statusFunc, 2, 1, 7, ["A few seconds remaining -- 2.3 of 2.4 KB (58 bytes/sec)", 1.724]);
+ testStatus(statusFunc, 1, 2, 6, ["A few seconds remaining -- 100 bytes of 2.4 KB (1.4 KB/sec)", 1.582]);
+ testStatus(statusFunc, 4, 3, 9, ["A few seconds remaining -- 959 KB of 1.0 MB (12.9 MB/sec)", 0.004]);
+ testStatus(statusFunc, 2, 3, 8, ["A few seconds remaining -- 2.3 of 56.5 KB (9.2 GB/sec)", 0.000]);
+
+ testStatus(statusFunc, 8, 4, 3, ["17 seconds remaining -- 9.2 of 9.2 GB (54.3 KB/sec)", 17.682]);
+ testStatus(statusFunc, 1, 3, 2, ["23 seconds remaining -- 100 bytes of 54.4 KB (2.3 KB/sec)", 23.691]);
+ testStatus(statusFunc, 9, 3, 2, ["23 seconds remaining -- 12.9 of 12.9 MB (2.3 KB/sec)", 23.691]);
+ testStatus(statusFunc, 5, 6, 7, ["25 seconds remaining -- 22.1 of 22.1 MB (58 bytes/sec)", 25.552]);
+
+ testStatus(statusFunc, 3, 9, 3, ["4 minutes remaining -- 54.3 KB of 12.9 MB (54.3 KB/sec)", 242.969]);
+ testStatus(statusFunc, 2, 3, 1, ["9 minutes remaining -- 2.3 of 56.5 KB (100 bytes/sec)", 555.550]);
+ testStatus(statusFunc, 4, 3, 7, ["15 minutes remaining -- 959 KB of 1.0 MB (58 bytes/sec)", 957.845]);
+ testStatus(statusFunc, 5, 3, 7, ["15 minutes remaining -- 22.1 of 22.2 MB (58 bytes/sec)", 957.845]);
+
+ testStatus(statusFunc, 1, 9, 2, ["1 hour, 35 minutes remaining -- 100 bytes of 12.9 MB (2.3 KB/sec)", 5756.133]);
+ testStatus(statusFunc, 2, 9, 6, ["2 hours, 31 minutes remaining -- 2.3 KB of 12.9 MB (1.4 KB/sec)", 9108.051]);
+ testStatus(statusFunc, 2, 4, 1, ["2 hours, 43 minutes remaining -- 2.3 of 962 KB (100 bytes/sec)", 9823.410]);
+ testStatus(statusFunc, 6, 4, 7, ["4 hours, 42 minutes remaining -- 1.4 of 961 KB (58 bytes/sec)", 16936.914]);
+
+ testStatus(statusFunc, 6, 9, 1, ["1 day, 13 hours remaining -- 1.4 KB of 12.9 MB (100 bytes/sec)", 134981.320]);
+ testStatus(statusFunc, 3, 8, 3, ["2 days, 1 hour remaining -- 54.3 KB of 9.2 GB (54.3 KB/sec)", 178596.872]);
+ testStatus(statusFunc, 1, 8, 6, ["77 days, 11 hours remaining -- 100 bytes of 9.2 GB (1.4 KB/sec)", 6694972.470]);
+ testStatus(statusFunc, 6, 8, 7, ["1979 days, 22 hours remaining -- 1.4 KB of 9.2 GB (58 bytes/sec)", 171068089.672]);
+
+ testStatus(statusFunc, 0, 0, 5, ["Unknown time remaining -- 0 of 0 bytes (22.1 MB/sec)", Infinity]);
+ testStatus(statusFunc, 0, 6, 0, ["Unknown time remaining -- 0 bytes of 1.4 KB (0 bytes/sec)", Infinity]);
+ testStatus(statusFunc, 6, 6, 0, ["Unknown time remaining -- 1.4 of 2.9 KB (0 bytes/sec)", Infinity]);
+ testStatus(statusFunc, 8, 5, 0, ["Unknown time remaining -- 9.2 of 9.3 GB (0 bytes/sec)", Infinity]);
+
+ // With rate equal to Infinity
+ testStatus(statusFunc, 0, 0, 10, ["Unknown time remaining -- 0 of 0 bytes (Really fast)", Infinity]);
+ testStatus(statusFunc, 1, 2, 10, ["A few seconds remaining -- 100 bytes of 2.4 KB (Really fast)", 0]);
+
+ // Now test without rates, via getDownloadStatusNoRate.
+ statusFunc = DownloadUtils.getDownloadStatusNoRate.bind(DownloadUtils);
+
+ testStatus(statusFunc, 2, 1, 7, ["A few seconds remaining -- 2.3 of 2.4 KB", 1.724]);
+ testStatus(statusFunc, 1, 2, 6, ["A few seconds remaining -- 100 bytes of 2.4 KB", 1.582]);
+ testStatus(statusFunc, 4, 3, 9, ["A few seconds remaining -- 959 KB of 1.0 MB", 0.004]);
+ testStatus(statusFunc, 2, 3, 8, ["A few seconds remaining -- 2.3 of 56.5 KB", 0.000]);
+
+ testStatus(statusFunc, 8, 4, 3, ["17 seconds remaining -- 9.2 of 9.2 GB", 17.682]);
+ testStatus(statusFunc, 1, 3, 2, ["23 seconds remaining -- 100 bytes of 54.4 KB", 23.691]);
+ testStatus(statusFunc, 9, 3, 2, ["23 seconds remaining -- 12.9 of 12.9 MB", 23.691]);
+ testStatus(statusFunc, 5, 6, 7, ["25 seconds remaining -- 22.1 of 22.1 MB", 25.552]);
+
+ testStatus(statusFunc, 3, 9, 3, ["4 minutes remaining -- 54.3 KB of 12.9 MB", 242.969]);
+ testStatus(statusFunc, 2, 3, 1, ["9 minutes remaining -- 2.3 of 56.5 KB", 555.550]);
+ testStatus(statusFunc, 4, 3, 7, ["15 minutes remaining -- 959 KB of 1.0 MB", 957.845]);
+ testStatus(statusFunc, 5, 3, 7, ["15 minutes remaining -- 22.1 of 22.2 MB", 957.845]);
+
+ testStatus(statusFunc, 1, 9, 2, ["1 hour, 35 minutes remaining -- 100 bytes of 12.9 MB", 5756.133]);
+ testStatus(statusFunc, 2, 9, 6, ["2 hours, 31 minutes remaining -- 2.3 KB of 12.9 MB", 9108.051]);
+ testStatus(statusFunc, 2, 4, 1, ["2 hours, 43 minutes remaining -- 2.3 of 962 KB", 9823.410]);
+ testStatus(statusFunc, 6, 4, 7, ["4 hours, 42 minutes remaining -- 1.4 of 961 KB", 16936.914]);
+
+ testStatus(statusFunc, 6, 9, 1, ["1 day, 13 hours remaining -- 1.4 KB of 12.9 MB", 134981.320]);
+ testStatus(statusFunc, 3, 8, 3, ["2 days, 1 hour remaining -- 54.3 KB of 9.2 GB", 178596.872]);
+ testStatus(statusFunc, 1, 8, 6, ["77 days, 11 hours remaining -- 100 bytes of 9.2 GB", 6694972.470]);
+ testStatus(statusFunc, 6, 8, 7, ["1979 days, 22 hours remaining -- 1.4 KB of 9.2 GB", 171068089.672]);
+
+ testStatus(statusFunc, 0, 0, 5, ["Unknown time remaining -- 0 of 0 bytes", Infinity]);
+ testStatus(statusFunc, 0, 6, 0, ["Unknown time remaining -- 0 bytes of 1.4 KB", Infinity]);
+ testStatus(statusFunc, 6, 6, 0, ["Unknown time remaining -- 1.4 of 2.9 KB", Infinity]);
+ testStatus(statusFunc, 8, 5, 0, ["Unknown time remaining -- 9.2 of 9.3 GB", Infinity]);
+
+ testURI("http://www.mozilla.org/", "mozilla.org", "www.mozilla.org");
+ testURI("http://www.city.mikasa.hokkaido.jp/", "city.mikasa.hokkaido.jp", "www.city.mikasa.hokkaido.jp");
+ testURI("data:text/html,Hello World", "data resource", "data resource");
+ testURI("jar:http://www.mozilla.com/file!/magic", "mozilla.com", "www.mozilla.com");
+ testURI("file:///C:/Cool/Stuff/", "local file", "local file");
+ // Don't test for moz-icon if we don't have a protocol handler for it (e.g. b2g):
+ if ("@mozilla.org/network/protocol;1?name=moz-icon" in Components.classes) {
+ testURI("moz-icon:file:///test.extension", "local file", "local file");
+ testURI("moz-icon://.extension", "moz-icon resource", "moz-icon resource");
+ }
+ testURI("about:config", "about resource", "about resource");
+ testURI("invalid.uri", "", "");
+
+ testAllGetReadableDates();
+}
diff --git a/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js b/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js
new file mode 100644
index 000000000..75eff3370
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/test_lowMinutes.js
@@ -0,0 +1,55 @@
+/* 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/. */
+
+/**
+ * Test bug 448344 to make sure when we're in low minutes, we show both minutes
+ * and seconds; but continue to show only minutes when we have plenty.
+ */
+
+Components.utils.import("resource://gre/modules/DownloadUtils.jsm");
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ * Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+var _ = function(some, debug, text, to) {
+ print(Array.slice(arguments).join(" "));
+};
+
+_("Make an array of time lefts and expected string to be shown for that time");
+var expectedTimes = [
+ [1.1, "A few seconds remaining", "under 4sec -> few"],
+ [2.5, "A few seconds remaining", "under 4sec -> few"],
+ [3.9, "A few seconds remaining", "under 4sec -> few"],
+ [5.3, "5 seconds remaining", "truncate seconds"],
+ [1.1 * 60, "1 minute, 6 seconds remaining", "under 4min -> show sec"],
+ [2.5 * 60, "2 minutes, 30 seconds remaining", "under 4min -> show sec"],
+ [3.9 * 60, "3 minutes, 54 seconds remaining", "under 4min -> show sec"],
+ [5.3 * 60, "5 minutes remaining", "over 4min -> only show min"],
+ [1.1 * 3600, "1 hour, 6 minutes remaining", "over 1hr -> show min/sec"],
+ [2.5 * 3600, "2 hours, 30 minutes remaining", "over 1hr -> show min/sec"],
+ [3.9 * 3600, "3 hours, 54 minutes remaining", "over 1hr -> show min/sec"],
+ [5.3 * 3600, "5 hours, 18 minutes remaining", "over 1hr -> show min/sec"],
+];
+_(expectedTimes.join("\n"));
+
+function run_test()
+{
+ expectedTimes.forEach(function([time, expectStatus, comment]) {
+ _("Running test with time", time);
+ _("Test comment:", comment);
+ let [status, last] = DownloadUtils.getTimeLeft(time);
+
+ _("Got status:", status, "last:", last);
+ _("Expecting..", expectStatus);
+ do_check_eq(status, expectStatus);
+
+ _();
+ });
+}
diff --git a/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js b/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js
new file mode 100644
index 000000000..86d810a9b
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/test_syncedDownloadUtils.js
@@ -0,0 +1,26 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Test bug 420482 by making sure multiple consumers of DownloadUtils gets the
+ * same time remaining time if they provide the same time left but a different
+ * "last time".
+ */
+
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+
+function run_test()
+{
+ // Simulate having multiple downloads requesting time left
+ let downloadTimes = {};
+ for (let time of [1, 30, 60, 3456, 9999])
+ downloadTimes[time] = DownloadUtils.getTimeLeft(time)[0];
+
+ // Pretend we're a download status bar also asking for a time left, but we're
+ // using a different "last sec". We need to make sure we get the same time.
+ let lastSec = 314;
+ for (let [time, text] of Object.entries(downloadTimes))
+ do_check_eq(DownloadUtils.getTimeLeft(time, lastSec)[0], text);
+}
diff --git a/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js b/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js
new file mode 100644
index 000000000..02e27c92c
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/test_unspecified_arguments.js
@@ -0,0 +1,25 @@
+/* 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/. */
+
+/**
+ * Make sure passing null and nothing to various variable-arg DownloadUtils
+ * methods provide the same result.
+ */
+
+var Cu = Components.utils;
+Cu.import("resource://gre/modules/DownloadUtils.jsm");
+
+function run_test()
+{
+ do_check_eq(DownloadUtils.getDownloadStatus(1000, null, null, null) + "",
+ DownloadUtils.getDownloadStatus(1000) + "");
+ do_check_eq(DownloadUtils.getDownloadStatus(1000, null, null) + "",
+ DownloadUtils.getDownloadStatus(1000, null) + "");
+
+ do_check_eq(DownloadUtils.getTransferTotal(1000, null) + "",
+ DownloadUtils.getTransferTotal(1000) + "");
+
+ do_check_eq(DownloadUtils.getTimeLeft(1000, null) + "",
+ DownloadUtils.getTimeLeft(1000) + "");
+}
diff --git a/toolkit/mozapps/downloads/tests/unit/xpcshell.ini b/toolkit/mozapps/downloads/tests/unit/xpcshell.ini
new file mode 100644
index 000000000..877816ef6
--- /dev/null
+++ b/toolkit/mozapps/downloads/tests/unit/xpcshell.ini
@@ -0,0 +1,10 @@
+[DEFAULT]
+head = head_downloads.js
+tail =
+skip-if = toolkit == 'android'
+
+[test_DownloadPaths.js]
+[test_DownloadUtils.js]
+[test_lowMinutes.js]
+[test_syncedDownloadUtils.js]
+[test_unspecified_arguments.js]
diff --git a/toolkit/mozapps/extensions/.eslintrc.js b/toolkit/mozapps/extensions/.eslintrc.js
new file mode 100644
index 000000000..2b90bd053
--- /dev/null
+++ b/toolkit/mozapps/extensions/.eslintrc.js
@@ -0,0 +1,8 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "rules": {
+ // No using undeclared variables
+ "no-undef": "error",
+ }
+};
diff --git a/toolkit/mozapps/extensions/AddonContentPolicy.cpp b/toolkit/mozapps/extensions/AddonContentPolicy.cpp
new file mode 100644
index 000000000..90e53b2ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonContentPolicy.cpp
@@ -0,0 +1,478 @@
+/* -*- 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/extensions/AddonContentPolicy.h b/toolkit/mozapps/extensions/AddonContentPolicy.h
new file mode 100644
index 000000000..4c8af4828
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonContentPolicy.h
@@ -0,0 +1,22 @@
+/* -*- 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/extensions/AddonManager.jsm b/toolkit/mozapps/extensions/AddonManager.jsm
new file mode 100644
index 000000000..c5cb80091
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManager.jsm
@@ -0,0 +1,3674 @@
+/* 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_BASE = "extensions.checkCompatibility";
+var PREF_EM_CHECK_COMPATIBILITY = MOZ_COMPATIBILITY_NIGHTLY ?
+ PREF_EM_CHECK_COMPATIBILITY_BASE + ".nightly" :
+ undefined;
+
+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();
+ }
+
+ if (!MOZ_COMPATIBILITY_NIGHTLY) {
+ PREF_EM_CHECK_COMPATIBILITY = PREF_EM_CHECK_COMPATIBILITY_BASE + "." +
+ Services.appinfo.version.replace(BRANCH_REGEXP, "$1");
+ }
+
+ 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/extensions/AddonManagerWebAPI.cpp b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
new file mode 100644
index 000000000..3f2a7a529
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.cpp
@@ -0,0 +1,171 @@
+/* -*- 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/extensions/AddonManagerWebAPI.h b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
new file mode 100644
index 000000000..6830bc91f
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonManagerWebAPI.h
@@ -0,0 +1,33 @@
+/* -*- 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/extensions/AddonPathService.cpp b/toolkit/mozapps/extensions/AddonPathService.cpp
new file mode 100644
index 000000000..8a405c0ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonPathService.cpp
@@ -0,0 +1,258 @@
+/* -*- 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/extensions/AddonPathService.h b/toolkit/mozapps/extensions/AddonPathService.h
new file mode 100644
index 000000000..f739b018f
--- /dev/null
+++ b/toolkit/mozapps/extensions/AddonPathService.h
@@ -0,0 +1,55 @@
+//* -*- 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/extensions/ChromeManifestParser.jsm b/toolkit/mozapps/extensions/ChromeManifestParser.jsm
new file mode 100644
index 000000000..63f1db785
--- /dev/null
+++ b/toolkit/mozapps/extensions/ChromeManifestParser.jsm
@@ -0,0 +1,157 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = ["ChromeManifestParser"];
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+const Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+
+
+/**
+ * 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);
+ Cc["@mozilla.org/globalmessagemanager;1"].getService(Ci.nsIMessageBroadcaster)
+ .broadcastAsyncMessage(MSG_JAR_FLUSH, aJarFile.path);
+}
+
+
+/**
+ * Parses chrome manifest files.
+ */
+this.ChromeManifestParser = {
+
+ /**
+ * Reads and parses a chrome manifest file located at a specified URI, and all
+ * secondary manifests it references.
+ *
+ * @param aURI
+ * A nsIURI pointing to a chrome manifest.
+ * Typically a file: or jar: URI.
+ * @return Array of objects describing each manifest instruction, in the form:
+ * { type: instruction-type, baseURI: string-uri, args: [arguments] }
+ **/
+ parseSync: function(aURI) {
+ function parseLine(aLine) {
+ let line = aLine.trim();
+ if (line.length == 0 || line.charAt(0) == '#')
+ return;
+ let tokens = line.split(/\s+/);
+ let type = tokens.shift();
+ if (type == "manifest") {
+ let uri = NetUtil.newURI(tokens.shift(), null, aURI);
+ data = data.concat(this.parseSync(uri));
+ } else {
+ data.push({type: type, baseURI: baseURI, args: tokens});
+ }
+ }
+
+ let contents = "";
+ try {
+ if (aURI.scheme == "jar")
+ contents = this._readFromJar(aURI);
+ else
+ contents = this._readFromFile(aURI);
+ } catch (e) {
+ // Silently fail.
+ }
+
+ if (!contents)
+ return [];
+
+ let baseURI = NetUtil.newURI(".", null, aURI).spec;
+
+ let data = [];
+ let lines = contents.split("\n");
+ lines.forEach(parseLine.bind(this));
+ return data;
+ },
+
+ _readFromJar: function(aURI) {
+ let data = "";
+ let entries = [];
+ let readers = [];
+
+ try {
+ // Deconstrict URI, which can be nested jar: URIs.
+ let uri = aURI.clone();
+ while (uri instanceof Ci.nsIJARURI) {
+ entries.push(uri.JAREntry);
+ uri = uri.JARFile;
+ }
+
+ // Open the base jar.
+ let reader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ reader.open(uri.QueryInterface(Ci.nsIFileURL).file);
+ readers.push(reader);
+
+ // Open the nested jars.
+ for (let i = entries.length - 1; i > 0; i--) {
+ let innerReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ innerReader.openInner(reader, entries[i]);
+ readers.push(innerReader);
+ reader = innerReader;
+ }
+
+ // First entry is the actual file we want to read.
+ let zis = reader.getInputStream(entries[0]);
+ data = NetUtil.readInputStreamToString(zis, zis.available());
+ }
+ finally {
+ // Close readers in reverse order.
+ for (let i = readers.length - 1; i >= 0; i--) {
+ readers[i].close();
+ flushJarCache(readers[i].file);
+ }
+ }
+
+ return data;
+ },
+
+ _readFromFile: function(aURI) {
+ let file = aURI.QueryInterface(Ci.nsIFileURL).file;
+ if (!file.exists() || !file.isFile())
+ return "";
+
+ let data = "";
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ try {
+ fis.init(file, -1, -1, false);
+ data = NetUtil.readInputStreamToString(fis, fis.available());
+ } finally {
+ fis.close();
+ }
+ return data;
+ },
+
+ /**
+ * Detects if there were any instructions of a specified type in a given
+ * chrome manifest.
+ *
+ * @param aManifest
+ * Manifest data, as returned by ChromeManifestParser.parseSync().
+ * @param aType
+ * Instruction type to filter by.
+ * @return True if any matching instructions were found in the manifest.
+ */
+ hasType: function(aManifest, aType) {
+ return aManifest.some(entry => entry.type == aType);
+ }
+};
diff --git a/toolkit/mozapps/extensions/DeferredSave.jsm b/toolkit/mozapps/extensions/DeferredSave.jsm
new file mode 100644
index 000000000..89f82b265
--- /dev/null
+++ b/toolkit/mozapps/extensions/DeferredSave.jsm
@@ -0,0 +1,275 @@
+/* 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 Cu = Components.utils;
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+
+Cu.import("resource://gre/modules/osfile.jsm");
+/* globals OS*/
+Cu.import("resource://gre/modules/Promise.jsm");
+
+// Make it possible to mock out timers for testing
+var MakeTimer = () => Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+this.EXPORTED_SYMBOLS = ["DeferredSave"];
+
+// If delay parameter is not provided, default is 50 milliseconds.
+const DEFAULT_SAVE_DELAY_MS = 50;
+
+Cu.import("resource://gre/modules/Log.jsm");
+// Configure a logger at the parent 'DeferredSave' level to format
+// messages for all the modules under DeferredSave.*
+const DEFERREDSAVE_PARENT_LOGGER_ID = "DeferredSave";
+var parentLogger = Log.repository.getLogger(DEFERREDSAVE_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));
+
+// 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.
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+/**
+* 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();
+
+/**
+ * A module to manage deferred, asynchronous writing of data files
+ * to disk. Writing is deferred by waiting for a specified delay after
+ * a request to save the data, before beginning to write. If more than
+ * one save request is received during the delay, all requests are
+ * fulfilled by a single write.
+ *
+ * @constructor
+ * @param aPath
+ * String representing the full path of the file where the data
+ * is to be written.
+ * @param aDataProvider
+ * Callback function that takes no argument and returns the data to
+ * be written. If aDataProvider returns an ArrayBufferView, the
+ * bytes it contains are written to the file as is.
+ * If aDataProvider returns a String the data are UTF-8 encoded
+ * and then written to the file.
+ * @param [optional] aDelay
+ * The delay in milliseconds between the first saveChanges() call
+ * that marks the data as needing to be saved, and when the DeferredSave
+ * begins writing the data to disk. Default 50 milliseconds.
+ */
+this.DeferredSave = function(aPath, aDataProvider, aDelay) {
+ // Create a new logger (child of 'DeferredSave' logger)
+ // for use by this particular instance of DeferredSave object
+ let leafName = OS.Path.basename(aPath);
+ let logger_id = DEFERREDSAVE_PARENT_LOGGER_ID + "." + leafName;
+ this.logger = Log.repository.getLogger(logger_id);
+
+ // @type {Deferred|null}, null when no data needs to be written
+ // @resolves with the result of OS.File.writeAtomic when all writes complete
+ // @rejects with the error from OS.File.writeAtomic if the write fails,
+ // or with the error from aDataProvider() if that throws.
+ this._pending = null;
+
+ // @type {Promise}, completes when the in-progress write (if any) completes,
+ // kept as a resolved promise at other times to simplify logic.
+ // Because _deferredSave() always uses _writing.then() to execute
+ // its next action, we don't need a special case for whether a write
+ // is in progress - if the previous write is complete (and the _writing
+ // promise is already resolved/rejected), _writing.then() starts
+ // the next action immediately.
+ //
+ // @resolves with the result of OS.File.writeAtomic
+ // @rejects with the error from OS.File.writeAtomic
+ this._writing = Promise.resolve(0);
+
+ // Are we currently waiting for a write to complete
+ this.writeInProgress = false;
+
+ this._path = aPath;
+ this._dataProvider = aDataProvider;
+
+ this._timer = null;
+
+ // Some counters for telemetry
+ // The total number of times the file was written
+ this.totalSaves = 0;
+
+ // The number of times the data became dirty while
+ // another save was in progress
+ this.overlappedSaves = 0;
+
+ // Error returned by the most recent write (if any)
+ this._lastError = null;
+
+ if (aDelay && (aDelay > 0))
+ this._delay = aDelay;
+ else
+ this._delay = DEFAULT_SAVE_DELAY_MS;
+}
+
+this.DeferredSave.prototype = {
+ get dirty() {
+ return this._pending || this.writeInProgress;
+ },
+
+ get lastError() {
+ return this._lastError;
+ },
+
+ // Start the pending timer if data is dirty
+ _startTimer: function() {
+ if (!this._pending) {
+ return;
+ }
+
+ this.logger.debug("Starting timer");
+ if (!this._timer)
+ this._timer = MakeTimer();
+ this._timer.initWithCallback(() => this._deferredSave(),
+ this._delay, Ci.nsITimer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Mark the current stored data dirty, and schedule a flush to disk
+ * @return A Promise<integer> that will be resolved after the data is written to disk;
+ * the promise is resolved with the number of bytes written.
+ */
+ saveChanges: function() {
+ this.logger.debug("Save changes");
+ if (!this._pending) {
+ if (this.writeInProgress) {
+ this.logger.debug("Data changed while write in progress");
+ this.overlappedSaves++;
+ }
+ this._pending = Promise.defer();
+ // Wait until the most recent write completes or fails (if it hasn't already)
+ // and then restart our timer
+ this._writing.then(count => this._startTimer(), error => this._startTimer());
+ }
+ return this._pending.promise;
+ },
+
+ _deferredSave: function() {
+ let pending = this._pending;
+ this._pending = null;
+ let writing = this._writing;
+ this._writing = pending.promise;
+
+ // In either the success or the exception handling case, we don't need to handle
+ // the error from _writing here; it's already being handled in another then()
+ let toSave = null;
+ try {
+ toSave = this._dataProvider();
+ }
+ catch (e) {
+ this.logger.error("Deferred save dataProvider failed", e);
+ writing.then(null, error => {})
+ .then(count => {
+ pending.reject(e);
+ });
+ return;
+ }
+
+ writing.then(null, error => { return 0; })
+ .then(count => {
+ this.logger.debug("Starting write");
+ this.totalSaves++;
+ this.writeInProgress = true;
+
+ OS.File.writeAtomic(this._path, toSave, {tmpPath: this._path + ".tmp"})
+ .then(
+ result => {
+ this._lastError = null;
+ this.writeInProgress = false;
+ this.logger.debug("Write succeeded");
+ pending.resolve(result);
+ },
+ error => {
+ this._lastError = error;
+ this.writeInProgress = false;
+ this.logger.warn("Write failed", error);
+ pending.reject(error);
+ });
+ });
+ },
+
+ /**
+ * Immediately save the dirty data to disk, skipping
+ * the delay of normal operation. Note that the write
+ * still happens asynchronously in the worker
+ * thread from OS.File.
+ *
+ * There are four possible situations:
+ * 1) Nothing to flush
+ * 2) Data is not currently being written, in-memory copy is dirty
+ * 3) Data is currently being written, in-memory copy is clean
+ * 4) Data is being written and in-memory copy is dirty
+ *
+ * @return Promise<integer> that will resolve when all in-memory data
+ * has finished being flushed, returning the number of bytes
+ * written. If all in-memory data is clean, completes with the
+ * result of the most recent write.
+ */
+ flush: function() {
+ // If we have pending changes, cancel our timer and set up the write
+ // immediately (_deferredSave queues the write for after the most
+ // recent write completes, if it hasn't already)
+ if (this._pending) {
+ this.logger.debug("Flush called while data is dirty");
+ if (this._timer) {
+ this._timer.cancel();
+ this._timer = null;
+ }
+ this._deferredSave();
+ }
+
+ return this._writing;
+ }
+};
diff --git a/toolkit/mozapps/extensions/LightweightThemeManager.jsm b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
new file mode 100644
index 000000000..5dd41831d
--- /dev/null
+++ b/toolkit/mozapps/extensions/LightweightThemeManager.jsm
@@ -0,0 +1,909 @@
+/* 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/extensions/addonManager.js b/toolkit/mozapps/extensions/addonManager.js
new file mode 100644
index 000000000..d34cbaf62
--- /dev/null
+++ b/toolkit/mozapps/extensions/addonManager.js
@@ -0,0 +1,296 @@
+/* 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/extensions/amContentHandler.js b/toolkit/mozapps/extensions/amContentHandler.js
new file mode 100644
index 000000000..8dc4dfecd
--- /dev/null
+++ b/toolkit/mozapps/extensions/amContentHandler.js
@@ -0,0 +1,100 @@
+/* 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 XPI_CONTENT_TYPE = "application/x-xpinstall";
+const MSG_INSTALL_ADDONS = "WebInstallerInstallAddonsFromWebpage";
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function amContentHandler() {
+}
+
+amContentHandler.prototype = {
+ /**
+ * Handles a new request for an application/x-xpinstall file.
+ *
+ * @param aMimetype
+ * The mimetype of the file
+ * @param aContext
+ * The context passed to nsIChannel.asyncOpen
+ * @param aRequest
+ * The nsIRequest dealing with the content
+ */
+ handleContent: function(aMimetype, aContext, aRequest) {
+ if (aMimetype != XPI_CONTENT_TYPE)
+ throw Cr.NS_ERROR_WONT_HANDLE_CONTENT;
+
+ if (!(aRequest instanceof Ci.nsIChannel))
+ throw Cr.NS_ERROR_WONT_HANDLE_CONTENT;
+
+ let uri = aRequest.URI;
+
+ let window = null;
+ let callbacks = aRequest.notificationCallbacks ?
+ aRequest.notificationCallbacks :
+ aRequest.loadGroup.notificationCallbacks;
+ if (callbacks)
+ window = callbacks.getInterface(Ci.nsIDOMWindow);
+
+ aRequest.cancel(Cr.NS_BINDING_ABORTED);
+
+ let installs = {
+ uris: [uri.spec],
+ hashes: [null],
+ names: [null],
+ icons: [null],
+ mimetype: XPI_CONTENT_TYPE,
+ triggeringPrincipal: aRequest.loadInfo.triggeringPrincipal,
+ callbackID: -1
+ };
+
+ 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);
+ listener.wrappedJSObject.receiveMessage({
+ name: MSG_INSTALL_ADDONS,
+ target: element,
+ data: installs,
+ });
+ return;
+ }
+ }
+
+ // Fall back to sending through the message manager
+ let messageManager = window.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDocShell)
+ .QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIContentFrameMessageManager);
+
+ messageManager.sendAsyncMessage(MSG_INSTALL_ADDONS, installs);
+ },
+
+ classID: Components.ID("{7beb3ba8-6ec3-41b4-b67c-da89b8518922}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentHandler]),
+
+ log : function(aMsg) {
+ let msg = "amContentHandler.js: " + (aMsg.join ? aMsg.join("") : aMsg);
+ Cc["@mozilla.org/consoleservice;1"].getService(Ci.nsIConsoleService).
+ logStringMessage(msg);
+ dump(msg + "\n");
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([amContentHandler]);
diff --git a/toolkit/mozapps/extensions/amIAddonManager.idl b/toolkit/mozapps/extensions/amIAddonManager.idl
new file mode 100644
index 000000000..58a58b62d
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIAddonManager.idl
@@ -0,0 +1,29 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * A service to make some AddonManager functionality available to C++ callers.
+ * Javascript callers should still use AddonManager.jsm directly.
+ */
+[scriptable, function, uuid(7b45d82d-7ad5-48d7-9b05-f32eb9818cd4)]
+interface amIAddonManager : nsISupports
+{
+ /**
+ * 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
+ * The nsIURI to map
+ * @return
+ * true if the URI has been mapped successfully to an Addon ID
+ */
+ boolean mapURIToAddonID(in nsIURI aURI, out AUTF8String aID);
+};
diff --git a/toolkit/mozapps/extensions/amIAddonPathService.idl b/toolkit/mozapps/extensions/amIAddonPathService.idl
new file mode 100644
index 000000000..9c9197a61
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIAddonPathService.idl
@@ -0,0 +1,37 @@
+/* -*- 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 "nsISupports.idl"
+
+interface nsIURI;
+
+/**
+ * This service maps file system paths where add-ons reside to the ID
+ * of the add-on. Paths are added by the add-on manager. They can
+ * looked up by anyone.
+ */
+[scriptable, uuid(fcd9e270-dfb1-11e3-8b68-0800200c9a66)]
+interface amIAddonPathService : nsISupports
+{
+ /**
+ * Given a path to a file, return the ID of the add-on that the file belongs
+ * to. Returns an empty string if there is no add-on there. Note that if an
+ * add-on is located at /a/b/c, then looking up the path /a/b/c/d will return
+ * that add-on.
+ */
+ AString findAddonId(in AString path);
+
+ /**
+ * Call this function to inform the service that the given file system path is
+ * associated with the given add-on ID.
+ */
+ void insertPath(in AString path, in AString addonId);
+
+ /**
+ * Given a URI to a file, return the ID of the add-on that the file belongs
+ * to. Returns an empty string if there is no add-on there.
+ */
+ AString mapURIToAddonId(in nsIURI aURI);
+};
diff --git a/toolkit/mozapps/extensions/amIWebInstallListener.idl b/toolkit/mozapps/extensions/amIWebInstallListener.idl
new file mode 100644
index 000000000..eed108097
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIWebInstallListener.idl
@@ -0,0 +1,134 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIURI;
+interface nsIVariant;
+
+/**
+ * amIWebInstallInfo is used by the default implementation of
+ * amIWebInstallListener to communicate with the running application and allow
+ * it to warn the user about blocked installs and start the installs running.
+ */
+[scriptable, uuid(fa0b47a3-f819-47ac-bc66-4bd1d7f67b1d)]
+interface amIWebInstallInfo : nsISupports
+{
+ readonly attribute nsIDOMElement browser;
+ readonly attribute nsIURI originatingURI;
+ readonly attribute nsIVariant installs;
+
+ /**
+ * Starts all installs.
+ */
+ void install();
+};
+
+/**
+ * The registered amIWebInstallListener is used to notify about new installs
+ * triggered by websites. The default implementation displays a confirmation
+ * dialog when add-ons are ready to install and uses the observer service to
+ * notify when installations are blocked.
+ */
+[scriptable, uuid(d9240d4b-6b3a-4cad-b402-de6c93337e0c)]
+interface amIWebInstallListener : nsISupports
+{
+ /**
+ * Called when installation by websites is currently disabled.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ void onWebInstallDisabled(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+
+ /**
+ * Called when the website is not allowed to directly prompt the user to
+ * install add-ons.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ * @return true if the caller should start the installs
+ */
+ boolean onWebInstallBlocked(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+
+ /**
+ * Called when a website wants to ask the user to install add-ons.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were requested
+ * @param aCount
+ * The number of AddonInstalls
+ * @return true if the caller should start the installs
+ */
+ boolean onWebInstallRequested(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
+
+[scriptable, uuid(a80b89ad-bb1a-4c43-9cb7-3ae656556f78)]
+interface amIWebInstallListener2 : nsISupports
+{
+ /**
+ * Called when a non-same-origin resource attempted to initiate an install.
+ * Installs will have already been cancelled and cannot be restarted.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were blocked
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ boolean onWebInstallOriginBlocked(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
+
+/**
+ * amIWebInstallPrompt is used, if available, by the default implementation of
+ * amIWebInstallInfo to display a confirmation UI to the user before running
+ * installs.
+ */
+[scriptable, uuid(386906f1-4d18-45bf-bc81-5dcd68e42c3b)]
+interface amIWebInstallPrompt : nsISupports
+{
+ /**
+ * Get a confirmation that the user wants to start the installs.
+ *
+ * @param aBrowser
+ * The browser that triggered the installs
+ * @param aUri
+ * The URI of the site that triggered the installs
+ * @param aInstalls
+ * The AddonInstalls that were requested
+ * @param aCount
+ * The number of AddonInstalls
+ */
+ void confirm(in nsIDOMElement aBrowser, in nsIURI aUri,
+ [array, size_is(aCount)] in nsIVariant aInstalls,
+ [optional] in uint32_t aCount);
+};
diff --git a/toolkit/mozapps/extensions/amIWebInstaller.idl b/toolkit/mozapps/extensions/amIWebInstaller.idl
new file mode 100644
index 000000000..6c5ebca67
--- /dev/null
+++ b/toolkit/mozapps/extensions/amIWebInstaller.idl
@@ -0,0 +1,82 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#include "nsISupports.idl"
+
+interface nsIDOMElement;
+interface nsIVariant;
+interface nsIURI;
+
+/**
+ * A callback function used to notify webpages when a requested install has
+ * ended.
+ *
+ * NOTE: This is *not* the same as InstallListener.
+ */
+[scriptable, function, uuid(bb22f5c0-3ca1-48f6-873c-54e87987700f)]
+interface amIInstallCallback : nsISupports
+{
+ /**
+ * Called when an install completes or fails.
+ *
+ * @param aUrl
+ * The url of the add-on being installed
+ * @param aStatus
+ * 0 if the install was successful or negative if not
+ */
+ void onInstallEnded(in AString aUrl, in int32_t aStatus);
+};
+
+
+/**
+ * This interface is used to allow webpages to start installing add-ons.
+ */
+[scriptable, uuid(658d6c09-15e0-4688-bee8-8551030472a9)]
+interface amIWebInstaller : nsISupports
+{
+ /**
+ * Checks if installation is enabled for a webpage.
+ *
+ * @param aMimetype
+ * The mimetype for the add-on to be installed
+ * @param referer
+ * The URL of the webpage trying to install an add-on
+ * @return true if installation is enabled
+ */
+ boolean isInstallEnabled(in AString aMimetype, in nsIURI aReferer);
+
+ /**
+ * Installs an array of add-ons at the request of a webpage
+ *
+ * @param aMimetype
+ * The mimetype for the add-ons
+ * @param aBrowser
+ * The browser installing the add-ons.
+ * @param aReferer
+ * The URI for the webpage installing the add-ons
+ * @param aUris
+ * The URIs of add-ons to be installed
+ * @param aHashes
+ * The hashes for the add-ons to be installed
+ * @param aNames
+ * The names for the add-ons to be installed
+ * @param aIcons
+ * The icons for the add-ons to be installed
+ * @param aCallback
+ * An optional callback to notify about installation success and
+ * failure
+ * @param aInstallCount
+ * An optional argument including the number of add-ons to install
+ * @return true if the installation was successfully started
+ */
+ boolean installAddonsFromWebpage(in AString aMimetype,
+ in nsIDOMElement aBrowser,
+ in nsIURI aReferer,
+ [array, size_is(aInstallCount)] in wstring aUris,
+ [array, size_is(aInstallCount)] in wstring aHashes,
+ [array, size_is(aInstallCount)] in wstring aNames,
+ [array, size_is(aInstallCount)] in wstring aIcons,
+ [optional] in amIInstallCallback aCallback,
+ [optional] in uint32_t aInstallCount);
+};
diff --git a/toolkit/mozapps/extensions/amInstallTrigger.js b/toolkit/mozapps/extensions/amInstallTrigger.js
new file mode 100644
index 000000000..382791d32
--- /dev/null
+++ b/toolkit/mozapps/extensions/amInstallTrigger.js
@@ -0,0 +1,240 @@
+/* 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/extensions/amWebAPI.js b/toolkit/mozapps/extensions/amWebAPI.js
new file mode 100644
index 000000000..5ad0d23f1
--- /dev/null
+++ b/toolkit/mozapps/extensions/amWebAPI.js
@@ -0,0 +1,269 @@
+/* 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/extensions/amWebInstallListener.js b/toolkit/mozapps/extensions/amWebInstallListener.js
new file mode 100644
index 000000000..0bcc345e8
--- /dev/null
+++ b/toolkit/mozapps/extensions/amWebInstallListener.js
@@ -0,0 +1,348 @@
+/* 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/extensions/content/OpenH264-license.txt b/toolkit/mozapps/extensions/content/OpenH264-license.txt
new file mode 100644
index 000000000..ad37989b8
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/OpenH264-license.txt
@@ -0,0 +1,59 @@
+-------------------------------------------------------
+About The Cisco-Provided Binary of OpenH264 Video Codec
+-------------------------------------------------------
+
+Cisco provides this program under the terms of the BSD license.
+
+Additionally, this binary is licensed under Cisco’s AVC/H.264 Patent Portfolio License from MPEG LA, at no cost to you, provided that the requirements and conditions shown below in the AVC/H.264 Patent Portfolio sections are met.
+
+As with all AVC/H.264 codecs, you may also obtain your own patent license from MPEG LA or from the individual patent owners, or proceed at your own risk. Your rights from Cisco under the BSD license are not affected by this choice.
+
+For more information on the OpenH264 binary licensing, please see the OpenH264 FAQ found at http://www.openh264.org/faq.html#binary
+
+A corresponding source code to this binary program is available under the same BSD terms, which can be found at http://www.openh264.org
+
+-----------
+BSD License
+-----------
+
+Copyright © 2014 Cisco Systems, Inc.
+
+All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer.
+
+2. 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.
+
+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 HOLDER 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.
+
+-----------------------------------------
+AVC/H.264 Patent Portfolio License Notice
+-----------------------------------------
+
+The binary form of this Software is distributed by Cisco under the AVC/H.264 Patent Portfolio License from MPEG LA, and is subject to the following requirements, which may or may not be applicable to your use of this software:
+
+THIS PRODUCT IS LICENSED UNDER THE AVC PATENT PORTFOLIO LICENSE FOR THE PERSONAL USE OF A CONSUMER OR OTHER USES IN WHICH IT DOES NOT RECEIVE REMUNERATION TO (i) ENCODE VIDEO IN COMPLIANCE WITH THE AVC STANDARD (“AVC VIDEOâ€) AND/OR (ii) DECODE AVC VIDEO THAT WAS ENCODED BY A CONSUMER ENGAGED IN A PERSONAL ACTIVITY AND/OR WAS OBTAINED FROM A VIDEO PROVIDER LICENSED TO PROVIDE AVC VIDEO. NO LICENSE IS GRANTED OR SHALL BE IMPLIED FOR ANY OTHER USE. ADDITIONAL INFORMATION MAY BE OBTAINED FROM MPEG LA, L.L.C. SEE HTTP://WWW.MPEGLA.COM
+
+Accordingly, please be advised that content providers and broadcasters using AVC/H.264 in their service may be required to obtain a separate use license from MPEG LA, referred to as "(b) sublicenses" in the SUMMARY OF AVC/H.264 LICENSE TERMS from MPEG LA found at http://www.openh264.org/mpegla
+
+---------------------------------------------
+AVC/H.264 Patent Portfolio License Conditions
+---------------------------------------------
+
+In addition, the Cisco-provided binary of this Software is licensed under Cisco's license from MPEG LA only if the following conditions are met:
+
+1. The Cisco-provided binary is separately downloaded to an end user’s device, and not integrated into or combined with third party software prior to being downloaded to the end user’s device;
+
+2. The end user must have the ability to control (e.g., to enable, disable, or re-enable) the use of the Cisco-provided binary;
+
+3. Third party software, in the location where end users can control the use of the Cisco-provided binary, must display the following text:
+
+ "OpenH264 Video Codec provided by Cisco Systems, Inc."
+
+4. Any third-party software that makes use of the Cisco-provided binary must reproduce all of the above text, as well as this last condition, in the EULA and/or in another location where licensing information is to be presented to the end user.
+
+
+
+ v1.0
diff --git a/toolkit/mozapps/extensions/content/about.js b/toolkit/mozapps/extensions/content/about.js
new file mode 100644
index 000000000..4f8fb353e
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/about.js
@@ -0,0 +1,103 @@
+// -*- 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/extensions/content/about.xul b/toolkit/mozapps/extensions/content/about.xul
new file mode 100644
index 000000000..6effcf37a
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/about.xul
@@ -0,0 +1,57 @@
+<?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/about.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://mozapps/locale/extensions/about.dtd">
+
+<dialog id="genericAbout"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();"
+ buttons="accept"
+ buttoniconaccept="close"
+ onaccept="close();">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/about.js"/>
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+
+ <stringbundleset id="aboutSet">
+ <stringbundle id="extensionsStrings" src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ </stringbundleset>
+
+ <vbox id="clientBox" flex="1">
+ <hbox class="basic-info">
+ <vbox pack="center">
+ <image id="extensionIcon"/>
+ </vbox>
+ <vbox flex="1">
+ <label id="extensionName"/>
+ <label id="extensionVersion" crop="end"/>
+ </vbox>
+ </hbox>
+ <description id="extensionDescription" class="boxIndent"/>
+
+ <separator id="groove" class="groove"/>
+
+ <vbox id="extensionDetailsBox" flex="1">
+ <label id="extensionCreatorLabel" class="sectionTitle">&creator.label;</label>
+ <hbox id="creatorBox" class="boxIndent">
+ <label id="extensionCreator" flex="1" crop="end"/>
+ <label id="extensionHomepage" onclick="if (event.button == 0) { loadHomepage(event); }"
+ class="text-link" value="&homepage.label;"/>
+ </hbox>
+
+ <label id="extensionDevelopers" class="sectionTitle">&developers.label;</label>
+ <vbox flex="1" id="developersBox" class="boxIndent"/>
+ <label id="extensionTranslators" class="sectionTitle">&translators.label;</label>
+ <vbox flex="1" id="translatorsBox" class="boxIndent"/>
+ <label id="extensionContributors" class="sectionTitle">&contributors.label;</label>
+ <vbox flex="1" id="contributorsBox" class="boxIndent"/>
+ </vbox>
+ </vbox>
+
+</dialog>
diff --git a/toolkit/mozapps/extensions/content/blocklist.css b/toolkit/mozapps/extensions/content/blocklist.css
new file mode 100644
index 000000000..cb48005a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/blocklist.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+.hardBlockedAddon {
+ -moz-binding: url("chrome://mozapps/content/extensions/blocklist.xml#hardblockedaddon");
+}
+
+.softBlockedAddon {
+ -moz-binding: url("chrome://mozapps/content/extensions/blocklist.xml#softblockedaddon");
+}
diff --git a/toolkit/mozapps/extensions/content/blocklist.js b/toolkit/mozapps/extensions/content/blocklist.js
new file mode 100644
index 000000000..6d524e6ee
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/blocklist.js
@@ -0,0 +1,72 @@
+// -*- 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";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var gArgs;
+
+function init() {
+ var hasHardBlocks = false;
+ var hasSoftBlocks = false;
+ gArgs = window.arguments[0].wrappedJSObject;
+
+ // NOTE: We use strings from the "updates.properties" bundleset to change the
+ // text on the "Cancel" button to "Restart Later". (bug 523784)
+ let bundle = Services.strings.
+ createBundle("chrome://mozapps/locale/update/updates.properties");
+ let cancelButton = document.documentElement.getButton("cancel");
+ cancelButton.setAttribute("label", bundle.GetStringFromName("restartLaterButton"));
+ cancelButton.setAttribute("accesskey",
+ bundle.GetStringFromName("restartLaterButton.accesskey"));
+
+ var richlist = document.getElementById("addonList");
+ var list = gArgs.list;
+ list.sort(function(a, b) { return String.localeCompare(a.name, b.name); });
+ for (let listItem of list) {
+ let item = document.createElement("richlistitem");
+ item.setAttribute("name", listItem.name);
+ item.setAttribute("version", listItem.version);
+ item.setAttribute("icon", listItem.icon);
+ if (listItem.blocked) {
+ item.setAttribute("class", "hardBlockedAddon");
+ hasHardBlocks = true;
+ }
+ else {
+ item.setAttribute("class", "softBlockedAddon");
+ hasSoftBlocks = true;
+ }
+ richlist.appendChild(item);
+ }
+
+ if (hasHardBlocks && hasSoftBlocks)
+ document.getElementById("bothMessage").hidden = false;
+ else if (hasHardBlocks)
+ document.getElementById("hardBlockMessage").hidden = false;
+ else
+ document.getElementById("softBlockMessage").hidden = false;
+
+ var link = document.getElementById("moreInfo");
+ if (list.length == 1 && list[0].url) {
+ link.setAttribute("href", list[0].url);
+ }
+ else {
+ var url = Services.urlFormatter.formatURLPref("extensions.blocklist.detailsURL");
+ link.setAttribute("href", url);
+ }
+}
+
+function finish(shouldRestartNow) {
+ gArgs.restart = shouldRestartNow;
+ var list = gArgs.list;
+ var items = document.getElementById("addonList").childNodes;
+ for (let i = 0; i < list.length; i++) {
+ if (!list[i].blocked)
+ list[i].disable = items[i].checked;
+ }
+ return true;
+}
diff --git a/toolkit/mozapps/extensions/content/blocklist.xml b/toolkit/mozapps/extensions/content/blocklist.xml
new file mode 100644
index 000000000..74474392f
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/blocklist.xml
@@ -0,0 +1,58 @@
+<?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 bindings [
+ <!ENTITY % blocklistDTD SYSTEM "chrome://mozapps/locale/extensions/blocklist.dtd" >
+ %blocklistDTD;
+]>
+
+<bindings id="blocklistBindings"
+ 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">
+
+ <binding id="hardblockedaddon">
+ <content align="start">
+ <xul:image xbl:inherits="src=icon"/>
+ <xul:vbox flex="1">
+ <xul:hbox class="addon-name-version">
+ <xul:label class="addonName" crop="end" xbl:inherits="value=name"/>
+ <xul:label class="addonVersion" xbl:inherits="value=version"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:spacer flex="1"/>
+ <xul:label class="blockedLabel" value="&blocklist.blocked.label;"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ </binding>
+
+ <binding id="softblockedaddon">
+ <content align="start">
+ <xul:image xbl:inherits="src=icon"/>
+ <xul:vbox flex="1">
+ <xul:hbox class="addon-name-version">
+ <xul:label class="addonName" crop="end" xbl:inherits="value=name"/>
+ <xul:label class="addonVersion" xbl:inherits="value=version"/>
+ </xul:hbox>
+ <xul:hbox>
+ <xul:spacer flex="1"/>
+ <xul:checkbox class="disableCheckbox" checked="true" label="&blocklist.checkbox.label;"/>
+ </xul:hbox>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <field name="_checkbox">
+ document.getAnonymousElementByAttribute(this, "class", "disableCheckbox")
+ </field>
+ <property name="checked" readonly="true">
+ <getter>
+ return this._checkbox.checked;
+ </getter>
+ </property>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/mozapps/extensions/content/blocklist.xul b/toolkit/mozapps/extensions/content/blocklist.xul
new file mode 100644
index 000000000..240d9e4e1
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/blocklist.xul
@@ -0,0 +1,46 @@
+<?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/"?>
+<?xml-stylesheet href="chrome://mozapps/skin/extensions/blocklist.css"?>
+<?xml-stylesheet href="chrome://mozapps/content/extensions/blocklist.css"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/blocklist.dtd">
+%extensionsDTD;
+]>
+
+<dialog windowtype="Addons:Blocklist" title="&blocklist.title;" align="stretch"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="init();" ondialogaccept="return finish(true)"
+ ondialogcancel="return finish(false)"
+ buttons="accept,cancel" style="&blocklist.style;"
+ buttonlabelaccept="&blocklist.accept.label;"
+ buttonaccesskeyaccept="&blocklist.accept.accesskey;">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/blocklist.js"/>
+
+ <hbox align="stretch" flex="1">
+ <vbox pack="start">
+ <image class="error-icon"/>
+ </vbox>
+ <vbox flex="1">
+ <label>&blocklist.summary;</label>
+ <separator class="thin"/>
+ <richlistbox id="addonList" flex="1"/>
+ <separator class="thin"/>
+ <description id="bothMessage" hidden="true" class="bold">&blocklist.softandhard;</description>
+ <description id="hardBlockMessage" hidden="true" class="bold">&blocklist.hardblocked;</description>
+ <description id="softBlockMessage" hidden="true" class="bold">&blocklist.softblocked;</description>
+ <hbox pack="start">
+ <label id="moreInfo" class="text-link" value="&blocklist.moreinfo;"/>
+ </hbox>
+ </vbox>
+ </hbox>
+</dialog>
diff --git a/toolkit/mozapps/extensions/content/eula.js b/toolkit/mozapps/extensions/content/eula.js
new file mode 100644
index 000000000..537ee7284
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/eula.js
@@ -0,0 +1,25 @@
+// -*- 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/extensions/content/eula.xul b/toolkit/mozapps/extensions/content/eula.xul
new file mode 100644
index 000000000..10e657951
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/eula.xul
@@ -0,0 +1,35 @@
+<?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/eula.css" type="text/css"?>
+
+<!DOCTYPE window [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%brandDTD;
+<!ENTITY % extensionsDTD SYSTEM "chrome://mozapps/locale/extensions/extensions.dtd">
+%extensionsDTD;
+]>
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&eula.title;" width="&eula.width;" height="&eula.height;"
+ buttons="accept,cancel" buttonlabelaccept="&eula.accept;"
+ ondialogaccept="window.arguments[0].accepted = true"
+ onload="Startup();">
+
+ <script type="application/javascript" src="chrome://mozapps/content/extensions/eula.js"/>
+
+ <stringbundleset id="extensionsSet">
+ <stringbundle id="extensionsStrings" src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ </stringbundleset>
+
+ <hbox id="heading-container">
+ <image id="icon"/>
+ <label id="heading" flex="1"/>
+ </hbox>
+
+ <textbox id="eula" multiline="true" readonly="true" flex="1"/>
+</dialog>
diff --git a/toolkit/mozapps/extensions/content/extensions.css b/toolkit/mozapps/extensions/content/extensions.css
new file mode 100644
index 000000000..cb5313365
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/extensions.css
@@ -0,0 +1,270 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+@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/extensions/content/extensions.js b/toolkit/mozapps/extensions/content/extensions.js
new file mode 100644
index 000000000..56158d9c6
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/extensions.js
@@ -0,0 +1,3915 @@
+/* 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");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Experiments",
+ "resource:///modules/experiments/Experiments.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;
+}
+
+function getExperimentEndDate(aAddon) {
+ if (!("@mozilla.org/browser/experiments-service;1" in Cc)) {
+ return 0;
+ }
+
+ if (!aAddon.isActive) {
+ return aAddon.endDate;
+ }
+
+ let experiment = Experiments.instance().getActiveExperiment();
+ if (!experiment) {
+ return 0;
+ }
+
+ return experiment.endDate;
+}
+
+/**
+ * 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_experimentsLearnMore: {
+ isEnabled: function() {
+ let mainWindow = getMainWindow();
+ return mainWindow && "switchToTabHavingURI" in mainWindow;
+ },
+ doCommand: function() {
+ let url = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
+ openOptionsInTab(url);
+ },
+ },
+
+ cmd_experimentsOpenTelemetryPreferences: {
+ isEnabled: function() {
+ return !!getMainWindowWithPreferencesPane();
+ },
+ doCommand: function() {
+ let mainWindow = getMainWindowWithPreferencesPane();
+ mainWindow.openAdvancedPreferences("dataChoicesTab");
+ },
+ },
+
+ 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 experiments.
+ if (aAddon.type == "experiment")
+ 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);
+
+ if (aObj.type == "experiment") {
+ item.endDate = getExperimentEndDate(aObj);
+ }
+
+ 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;
+ }
+
+ if (Preferences.get("plugin.load_flash_only", true)) {
+ document.getElementById("plugindeprecation-learnmore-link")
+ .setAttribute("href", Services.urlFormatter.formatURLPref("app.support.baseURL") + "npapi");
+ } else {
+ 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);
+
+ if (aInstall.addon.type == "experiment") {
+ let item = this.getListItemForID(aInstall.addon.id);
+ if (item) {
+ item.endDate = getExperimentEndDate(aInstall.addon);
+ }
+ }
+ },
+
+ 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");
+ }
+ }
+
+ if (this._addon.type == "experiment") {
+ let prefix = "details.experiment.";
+ let active = this._addon.isActive;
+
+ let stateKey = prefix + "state." + (active ? "active" : "complete");
+ let node = document.getElementById("detail-experiment-state");
+ node.value = gStrings.ext.GetStringFromName(stateKey);
+
+ let now = Date.now();
+ let end = getExperimentEndDate(this._addon);
+ 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);
+ }
+
+ document.getElementById("detail-experiment-time").value = timeMessage;
+ }
+
+ 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/extensions/content/extensions.xml b/toolkit/mozapps/extensions/content/extensions.xml
new file mode 100644
index 000000000..b49645cf0
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/extensions.xml
@@ -0,0 +1,2008 @@
+<?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/extensions/content/extensions.xul b/toolkit/mozapps/extensions/content/extensions.xul
new file mode 100644
index 000000000..70939d024
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/extensions.xul
@@ -0,0 +1,715 @@
+<?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; &#160;
+ <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/extensions/content/gmpPrefs.xul b/toolkit/mozapps/extensions/content/gmpPrefs.xul
new file mode 100644
index 000000000..ea7ee92fa
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/gmpPrefs.xul
@@ -0,0 +1,8 @@
+<?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/. -->
+
+<!-- This is intentionally empty and a dummy to let the GMPProvider
+ have a preferences button in the list view. -->
diff --git a/toolkit/mozapps/extensions/content/list.js b/toolkit/mozapps/extensions/content/list.js
new file mode 100644
index 000000000..a31922703
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/list.js
@@ -0,0 +1,165 @@
+// -*- 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/. */
+
+const kXULNS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+const kDialog = "dialog";
+
+/**
+ * This dialog can be initialized from parameters supplied via window.arguments
+ * or it can be used to display blocklist notification and blocklist blocked
+ * installs via nsIDialogParamBlock as is done by nsIExtensionManager.
+ *
+ * When using this dialog with window.arguments it must be opened modally, the
+ * caller can inspect the user action after the dialog closes by inspecting the
+ * value of the |result| parameter on this object which is set to the dlgtype
+ * of the button used to close the dialog.
+ *
+ * window.arguments[0] is an array of strings to display in the tree. If the
+ * array is empty the tree will not be displayed.
+ * window.arguments[1] a JS Object with the following properties:
+ *
+ * title: A title string, to be displayed in the title bar of the dialog.
+ * message1: A message string, displayed above the addon list
+ * message2: A message string, displayed below the addon list
+ * message3: A bolded message string, displayed below the addon list
+ * moreInfoURL: An url for displaying more information
+ * iconClass : Can be one of the following values (default is alert-icon)
+ * alert-icon, error-icon, or question-icon
+ *
+ * If no value is supplied for message1, message2, message3, or moreInfoURL,
+ * the element is not displayed.
+ *
+ * buttons: {
+ * accept: { label: "A Label for the Accept button",
+ * focused: true },
+ * cancel: { label: "A Label for the Cancel button" },
+ * ...
+ * },
+ *
+ * result: The dlgtype of button that was used to dismiss the dialog.
+ */
+
+"use strict";
+
+var gButtons = { };
+
+function init() {
+ var de = document.documentElement;
+ var items = [];
+ if (window.arguments[0] instanceof Components.interfaces.nsIDialogParamBlock) {
+ // This is a warning about a blocklisted item the user is trying to install
+ var args = window.arguments[0];
+ var softblocked = args.GetInt(0) == 1 ? true : false;
+
+ var extensionsBundle = document.getElementById("extensionsBundle");
+ try {
+ var formatter = Components.classes["@mozilla.org/toolkit/URLFormatterService;1"]
+ .getService(Components.interfaces.nsIURLFormatter);
+ var url = formatter.formatURLPref("extensions.blocklist.detailsURL");
+ }
+ catch (e) { }
+
+ var params = {
+ moreInfoURL: url,
+ };
+
+ if (softblocked) {
+ params.title = extensionsBundle.getString("softBlockedInstallTitle");
+ params.message1 = extensionsBundle.getFormattedString("softBlockedInstallMsg",
+ [args.GetString(0)]);
+ var accept = de.getButton("accept");
+ accept.label = extensionsBundle.getString("softBlockedInstallAcceptLabel");
+ accept.accessKey = extensionsBundle.getString("softBlockedInstallAcceptKey");
+ de.getButton("cancel").focus();
+ document.addEventListener("dialogaccept", allowInstall, false);
+ }
+ else {
+ params.title = extensionsBundle.getString("blocklistedInstallTitle2");
+ params.message1 = extensionsBundle.getFormattedString("blocklistedInstallMsg2",
+ [args.GetString(0)]);
+ de.buttons = "accept";
+ de.getButton("accept").focus();
+ }
+ }
+ else {
+ items = window.arguments[0];
+ params = window.arguments[1];
+ }
+
+ var addons = document.getElementById("addonsChildren");
+ if (items.length > 0)
+ document.getElementById("addonsTree").hidden = false;
+
+ // Fill the addons list
+ for (var item of items) {
+ var treeitem = document.createElementNS(kXULNS, "treeitem");
+ var treerow = document.createElementNS(kXULNS, "treerow");
+ var treecell = document.createElementNS(kXULNS, "treecell");
+ treecell.setAttribute("label", item);
+ treerow.appendChild(treecell);
+ treeitem.appendChild(treerow);
+ addons.appendChild(treeitem);
+ }
+
+ // Set the messages
+ var messages = ["message1", "message2", "message3"];
+ for (let messageEntry of messages) {
+ if (messageEntry in params) {
+ var message = document.getElementById(messageEntry);
+ message.hidden = false;
+ message.appendChild(document.createTextNode(params[messageEntry]));
+ }
+ }
+
+ document.getElementById("infoIcon").className =
+ params["iconClass"] ? "spaced " + params["iconClass"] : "spaced alert-icon";
+
+ if ("moreInfoURL" in params && params["moreInfoURL"]) {
+ message = document.getElementById("moreInfo");
+ message.value = extensionsBundle.getString("moreInfoText");
+ message.setAttribute("href", params["moreInfoURL"]);
+ document.getElementById("moreInfoBox").hidden = false;
+ }
+
+ // Set the window title
+ if ("title" in params)
+ document.title = params.title;
+
+ // Set up the buttons
+ if ("buttons" in params) {
+ gButtons = params.buttons;
+ var buttonString = "";
+ for (var buttonType in gButtons)
+ buttonString += "," + buttonType;
+ de.buttons = buttonString.substr(1);
+ for (buttonType in gButtons) {
+ var button = de.getButton(buttonType);
+ button.label = gButtons[buttonType].label;
+ if (gButtons[buttonType].focused)
+ button.focus();
+ document.addEventListener(kDialog + buttonType, handleButtonCommand, true);
+ }
+ }
+}
+
+function shutdown() {
+ for (var buttonType in gButtons)
+ document.removeEventListener(kDialog + buttonType, handleButtonCommand, true);
+}
+
+function allowInstall() {
+ var args = window.arguments[0];
+ args.SetInt(1, 1);
+}
+
+/**
+ * Watch for the user hitting one of the buttons to dismiss the dialog
+ * and report the result back to the caller through the |result| property on
+ * the arguments object.
+ */
+function handleButtonCommand(event) {
+ window.arguments[1].result = event.type.substr(kDialog.length);
+}
diff --git a/toolkit/mozapps/extensions/content/list.xul b/toolkit/mozapps/extensions/content/list.xul
new file mode 100644
index 000000000..65efeb6a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/list.xul
@@ -0,0 +1,44 @@
+<?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/"?>
+
+<dialog id="addonList" windowtype="Addons:List"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onunload="shutdown();"
+ buttons="accept,cancel" onload="init();">
+
+ <script type="application/javascript" src="chrome://global/content/globalOverlay.js"/>
+ <script type="application/javascript"
+ src="chrome://mozapps/content/extensions/list.js"/>
+
+ <stringbundle id="extensionsBundle"
+ src="chrome://mozapps/locale/extensions/extensions.properties"/>
+ <stringbundle id="brandBundle"
+ src="chrome://branding/locale/brand.properties"/>
+
+ <hbox align="start">
+ <vbox>
+ <image id="infoIcon"/>
+ </vbox>
+ <vbox class="spaced" style="min-width: 20em; max-width: 40em">
+ <label id="message1" class="spaced" hidden="true"/>
+ <separator class="thin"/>
+ <tree id="addonsTree" rows="6" hidecolumnpicker="true" hidden="true" class="spaced">
+ <treecols style="max-width: 25em;">
+ <treecol flex="1" id="nameColumn" hideheader="true"/>
+ </treecols>
+ <treechildren id="addonsChildren"/>
+ </tree>
+ <label id="message2" class="spaced" hidden="true"/>
+ <label class="bold spaced" id="message3" hidden="true"/>
+ <hbox id="moreInfoBox" hidden="true">
+ <label id="moreInfo" class="text-link spaced"/>
+ <spacer flex="1"/>
+ </hbox>
+ </vbox>
+ </hbox>
+</dialog>
diff --git a/toolkit/mozapps/extensions/content/newaddon.js b/toolkit/mozapps/extensions/content/newaddon.js
new file mode 100644
index 000000000..b1ad5631b
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/newaddon.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/AddonManager.jsm");
+
+var gAddon = null;
+
+// If the user enables the add-on through some other UI close this window
+var EnableListener = {
+ onEnabling: function(aAddon) {
+ if (aAddon.id == gAddon.id)
+ window.close();
+ }
+}
+AddonManager.addAddonListener(EnableListener);
+
+function initialize() {
+ // About URIs don't implement nsIURL so we have to find the query string
+ // manually
+ let spec = document.location.href;
+ let pos = spec.indexOf("?");
+ let query = "";
+ if (pos >= 0)
+ query = spec.substring(pos + 1);
+
+ // Just assume the query is "id=<id>"
+ let id = query.substring(3);
+ if (!id) {
+ window.location = "about:blank";
+ return;
+ }
+
+ let bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/newaddon.properties");
+
+ AddonManager.getAddonByID(id, function(aAddon) {
+ // If the add-on doesn't exist or it is already enabled or it has already
+ // 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 || aAddon.seen ||
+ !(aAddon.permissions & AddonManager.PERM_CAN_ENABLE)) {
+ window.close();
+ return;
+ }
+
+ gAddon = aAddon;
+
+ document.getElementById("addon-info").setAttribute("type", aAddon.type);
+
+ let icon = document.getElementById("icon");
+ if (aAddon.icon64URL)
+ icon.src = aAddon.icon64URL;
+ else if (aAddon.iconURL)
+ icon.src = aAddon.iconURL;
+
+ let name = bundle.formatStringFromName("name", [aAddon.name, aAddon.version],
+ 2);
+ document.getElementById("name").value = name;
+
+ if (aAddon.creator) {
+ let creator = bundle.formatStringFromName("author", [aAddon.creator], 1);
+ document.getElementById("author").value = creator;
+ } else {
+ document.getElementById("author").hidden = true;
+ }
+
+ let uri = "getResourceURI" in aAddon ? aAddon.getResourceURI() : null;
+ let locationLabel = document.getElementById("location");
+ if (uri instanceof Ci.nsIFileURL) {
+ let location = bundle.formatStringFromName("location", [uri.file.path], 1);
+ locationLabel.value = location;
+ locationLabel.setAttribute("tooltiptext", location);
+ } else {
+ document.getElementById("location").hidden = true;
+ }
+
+ // 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);
+ }
+
+ var event = document.createEvent("Events");
+ event.initEvent("AddonDisplayed", true, true);
+ document.dispatchEvent(event);
+ });
+}
+
+function unload() {
+ AddonManager.removeAddonListener(EnableListener);
+}
+
+function continueClicked() {
+ AddonManager.removeAddonListener(EnableListener);
+
+ if (document.getElementById("allow").checked) {
+ gAddon.userDisabled = false;
+
+ if (gAddon.pendingOperations & AddonManager.PENDING_ENABLE) {
+ document.getElementById("allow").disabled = true;
+ document.getElementById("buttonDeck").selectedPanel = document.getElementById("restartPanel");
+ return;
+ }
+ }
+
+ window.close();
+}
+
+function restartClicked() {
+ 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
+
+ window.close();
+
+ let appStartup = Components.classes["@mozilla.org/toolkit/app-startup;1"].
+ getService(Components.interfaces.nsIAppStartup);
+ appStartup.quit(Ci.nsIAppStartup.eAttemptQuit | Ci.nsIAppStartup.eRestart);
+}
+
+function cancelClicked() {
+ gAddon.userDisabled = true;
+ AddonManager.addAddonListener(EnableListener);
+
+ document.getElementById("allow").disabled = false;
+ document.getElementById("buttonDeck").selectedPanel = document.getElementById("continuePanel");
+}
diff --git a/toolkit/mozapps/extensions/content/newaddon.xul b/toolkit/mozapps/extensions/content/newaddon.xul
new file mode 100644
index 000000000..1d8545249
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/newaddon.xul
@@ -0,0 +1,67 @@
+<?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/extensions/content/pluginPrefs.xul b/toolkit/mozapps/extensions/content/pluginPrefs.xul
new file mode 100644
index 000000000..c3fdbfa5b
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/pluginPrefs.xul
@@ -0,0 +1,20 @@
+<?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 window SYSTEM "chrome://pluginproblem/locale/pluginproblem.dtd">
+
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting type="control" title="&plugin.file;">
+ <label class="text-list" id="pluginLibraries"/>
+ </setting>
+ <setting type="control" title="&plugin.mimeTypes;">
+ <label class="text-list" id="pluginMimeTypes"/>
+ </setting>
+ <setting type="bool" pref="dom.ipc.plugins.flash.disable-protected-mode"
+ inverted="true" title="&plugin.flashProtectedMode.label;"
+ id="pluginEnableProtectedMode"
+ learnmore="https://support.mozilla.org/kb/flash-protected-mode-settings" />
+</vbox>
diff --git a/toolkit/mozapps/extensions/content/setting.xml b/toolkit/mozapps/extensions/content/setting.xml
new file mode 100644
index 000000000..2b70eb0d0
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/setting.xml
@@ -0,0 +1,486 @@
+<?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/extensions/content/update.js b/toolkit/mozapps/extensions/content/update.js
new file mode 100644
index 000000000..80d0fa688
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/update.js
@@ -0,0 +1,663 @@
+// -*- 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/extensions/content/update.xul b/toolkit/mozapps/extensions/content/update.xul
new file mode 100644
index 000000000..745983814
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/update.xul
@@ -0,0 +1,194 @@
+<?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/extensions/content/updateinfo.xsl b/toolkit/mozapps/extensions/content/updateinfo.xsl
new file mode 100644
index 000000000..5fcccd6d7
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/updateinfo.xsl
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+
+<xsl:stylesheet version="1.0" xmlns:xhtml="http://www.w3.org/1999/xhtml"
+ xmlns:xsl="http://www.w3.org/1999/XSL/Transform">
+
+ <!-- Any elements not otherwise specified will be stripped but the contents
+ will be displayed. All attributes are stripped from copied elements. -->
+
+ <!-- Block these elements and their contents -->
+ <xsl:template match="xhtml:head|xhtml:script|xhtml:style">
+ </xsl:template>
+
+ <!-- Allowable styling elements -->
+ <xsl:template match="xhtml:b|xhtml:i|xhtml:em|xhtml:strong|xhtml:u|xhtml:q|xhtml:sub|xhtml:sup|xhtml:code">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- Allowable block formatting elements -->
+ <xsl:template match="xhtml:h1|xhtml:h2|xhtml:h3|xhtml:p|xhtml:div|xhtml:blockquote|xhtml:pre">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- Allowable list formatting elements -->
+ <xsl:template match="xhtml:ul|xhtml:ol|xhtml:li|xhtml:dl|xhtml:dt|xhtml:dd">
+ <xsl:copy><xsl:apply-templates/></xsl:copy>
+ </xsl:template>
+
+ <!-- These elements are copied and their contents dropped -->
+ <xsl:template match="xhtml:br|xhtml:hr">
+ <xsl:copy/>
+ </xsl:template>
+
+ <!-- The root document -->
+ <xsl:template match="/">
+ <xhtml:body><xsl:apply-templates/></xhtml:body>
+ </xsl:template>
+
+</xsl:stylesheet>
diff --git a/toolkit/mozapps/extensions/content/xpinstallConfirm.css b/toolkit/mozapps/extensions/content/xpinstallConfirm.css
new file mode 100644
index 000000000..583facfec
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/xpinstallConfirm.css
@@ -0,0 +1,8 @@
+/* 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/. */
+
+installitem {
+ -moz-binding: url("chrome://mozapps/content/xpinstall/xpinstallItem.xml#installitem");
+ display: -moz-box;
+}
diff --git a/toolkit/mozapps/extensions/content/xpinstallConfirm.js b/toolkit/mozapps/extensions/content/xpinstallConfirm.js
new file mode 100644
index 000000000..5660cdaaf
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/xpinstallConfirm.js
@@ -0,0 +1,196 @@
+// -*- 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/. */
+
+var XPInstallConfirm = {};
+
+XPInstallConfirm.init = function()
+{
+ Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+ var _installCountdown;
+ var _installCountdownInterval;
+ var _focused;
+ var _timeout;
+
+ // Default to cancelling the install when the window unloads
+ XPInstallConfirm._installOK = false;
+
+ var bundle = document.getElementById("xpinstallConfirmStrings");
+
+ let args = window.arguments[0].wrappedJSObject;
+
+ // If all installs have already been cancelled in some way then just close
+ // the window
+ if (args.installs.every(i => i.state != AddonManager.STATE_DOWNLOADED)) {
+ window.close();
+ return;
+ }
+
+ var _installCountdownLength = 5;
+ try {
+ var prefs = Components.classes["@mozilla.org/preferences-service;1"]
+ .getService(Components.interfaces.nsIPrefBranch);
+ var delay_in_milliseconds = prefs.getIntPref("security.dialog_enable_delay");
+ _installCountdownLength = Math.round(delay_in_milliseconds / 500);
+ } catch (ex) { }
+
+ var itemList = document.getElementById("itemList");
+
+ let installMap = new WeakMap();
+ let installListener = {
+ onDownloadCancelled: function(install) {
+ itemList.removeChild(installMap.get(install));
+ if (--numItemsToInstall == 0)
+ window.close();
+ }
+ };
+
+ var numItemsToInstall = args.installs.length;
+ for (let install of args.installs) {
+ var installItem = document.createElement("installitem");
+ itemList.appendChild(installItem);
+
+ installItem.name = install.addon.name;
+ installItem.url = install.sourceURI.spec;
+ var icon = install.iconURL;
+ if (icon)
+ installItem.icon = icon;
+ var type = install.type;
+ if (type)
+ installItem.type = type;
+ if (install.certName) {
+ installItem.cert = bundle.getFormattedString("signed", [install.certName]);
+ }
+ else {
+ installItem.cert = bundle.getString("unverified");
+ }
+ installItem.signed = install.certName ? "true" : "false";
+
+ installMap.set(install, installItem);
+ install.addListener(installListener);
+ }
+
+ var introString = bundle.getString("itemWarnIntroSingle");
+ if (numItemsToInstall > 4)
+ introString = bundle.getFormattedString("itemWarnIntroMultiple", [numItemsToInstall]);
+ var textNode = document.createTextNode(introString);
+ var introNode = document.getElementById("itemWarningIntro");
+ while (introNode.hasChildNodes())
+ introNode.removeChild(introNode.firstChild);
+ introNode.appendChild(textNode);
+
+ var okButton = document.documentElement.getButton("accept");
+ okButton.focus();
+
+ function okButtonCountdown() {
+ _installCountdown -= 1;
+
+ if (_installCountdown < 1) {
+ okButton.label = bundle.getString("installButtonLabel");
+ okButton.disabled = false;
+ clearInterval(_installCountdownInterval);
+ }
+ else
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ }
+
+ function myfocus() {
+ // Clear the timeout if it exists so only the last one will be used.
+ if (_timeout)
+ clearTimeout(_timeout);
+
+ // Use setTimeout since the last focus or blur event to fire is the one we
+ // want
+ _timeout = setTimeout(setWidgetsAfterFocus, 0);
+ }
+
+ function setWidgetsAfterFocus() {
+ if (_focused)
+ return;
+
+ _installCountdown = _installCountdownLength;
+ _installCountdownInterval = setInterval(okButtonCountdown, 500);
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ _focused = true;
+ }
+
+ function myblur() {
+ // Clear the timeout if it exists so only the last one will be used.
+ if (_timeout)
+ clearTimeout(_timeout);
+
+ // Use setTimeout since the last focus or blur event to fire is the one we
+ // want
+ _timeout = setTimeout(setWidgetsAfterBlur, 0);
+ }
+
+ function setWidgetsAfterBlur() {
+ if (!_focused)
+ return;
+
+ // Set _installCountdown to the inital value set in setWidgetsAfterFocus
+ // plus 1 so when the window is focused there is immediate ui feedback.
+ _installCountdown = _installCountdownLength + 1;
+ okButton.label = bundle.getFormattedString("installButtonDisabledLabel", [_installCountdown]);
+ okButton.disabled = true;
+ clearInterval(_installCountdownInterval);
+ _focused = false;
+ }
+
+ function myUnload() {
+ if (_installCountdownLength > 0) {
+ document.removeEventListener("focus", myfocus, true);
+ document.removeEventListener("blur", myblur, true);
+ }
+ window.removeEventListener("unload", myUnload, false);
+
+ for (let install of args.installs)
+ install.removeListener(installListener);
+
+ // Now perform the desired action - either install the
+ // addons or cancel the installations
+ if (XPInstallConfirm._installOK) {
+ for (let install of args.installs)
+ install.install();
+ }
+ else {
+ for (let install of args.installs) {
+ if (install.state != AddonManager.STATE_CANCELLED)
+ install.cancel();
+ }
+ }
+ }
+
+ window.addEventListener("unload", myUnload, false);
+
+ if (_installCountdownLength > 0) {
+ document.addEventListener("focus", myfocus, true);
+ document.addEventListener("blur", myblur, true);
+
+ okButton.disabled = true;
+ setWidgetsAfterFocus();
+ }
+ else
+ okButton.label = bundle.getString("installButtonLabel");
+}
+
+XPInstallConfirm.onOK = function()
+{
+ Components.classes["@mozilla.org/base/telemetry;1"].
+ getService(Components.interfaces.nsITelemetry).
+ getHistogramById("SECURITY_UI").
+ add(Components.interfaces.nsISecurityUITelemetry.WARNING_CONFIRM_ADDON_INSTALL_CLICK_THROUGH);
+ // Perform the install or cancel after the window has unloaded
+ XPInstallConfirm._installOK = true;
+ return true;
+}
+
+XPInstallConfirm.onCancel = function()
+{
+ // Perform the install or cancel after the window has unloaded
+ XPInstallConfirm._installOK = false;
+ return true;
+}
diff --git a/toolkit/mozapps/extensions/content/xpinstallConfirm.xul b/toolkit/mozapps/extensions/content/xpinstallConfirm.xul
new file mode 100644
index 000000000..f1c29eb73
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/xpinstallConfirm.xul
@@ -0,0 +1,37 @@
+<?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://mozapps/content/xpinstall/xpinstallConfirm.css" type="text/css"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/xpinstall/xpinstallConfirm.css" type="text/css"?>
+
+<!DOCTYPE dialog SYSTEM "chrome://mozapps/locale/xpinstall/xpinstallConfirm.dtd">
+
+<dialog xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="xpinstallConfirm" title="&dialog.title;" style="&dialog.style;"
+ windowtype="Addons:Install"
+ onload="XPInstallConfirm.init()"
+ ondialogaccept="return XPInstallConfirm.onOK();"
+ ondialogcancel="return XPInstallConfirm.onCancel();">
+
+ <script src="chrome://mozapps/content/xpinstall/xpinstallConfirm.js" type="application/javascript"/>
+
+ <stringbundle id="xpinstallConfirmStrings"
+ src="chrome://mozapps/locale/xpinstall/xpinstallConfirm.properties"/>
+
+ <vbox flex="1" id="dialogContentBox">
+ <hbox id="xpinstallheader" align="start">
+ <image class="alert-icon"/>
+ <vbox flex="1">
+ <description class="warning">&warningPrimary.label;</description>
+ <description>&warningSecondary.label;</description>
+ </vbox>
+ </hbox>
+ <label id="itemWarningIntro"/>
+ <vbox id="itemList" class="listbox" flex="1" style="overflow: auto;"/>
+ </vbox>
+
+</dialog>
diff --git a/toolkit/mozapps/extensions/content/xpinstallItem.xml b/toolkit/mozapps/extensions/content/xpinstallItem.xml
new file mode 100644
index 000000000..5146af84f
--- /dev/null
+++ b/toolkit/mozapps/extensions/content/xpinstallItem.xml
@@ -0,0 +1,51 @@
+<?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 bindings SYSTEM "chrome://mozapps/locale/xpinstall/xpinstallConfirm.dtd">
+
+<bindings id="xpinstallItemBindings"
+ 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">
+
+ <binding id="installitem">
+ <resources>
+ <stylesheet src="chrome://mozapps/skin/xpinstall/xpinstallConfirm.css"/>
+ </resources>
+ <content>
+ <xul:hbox flex="1">
+ <xul:vbox align="center" pack="center" class="xpinstallIconContainer">
+ <xul:image class="xpinstallItemIcon" xbl:inherits="src=icon"/>
+ </xul:vbox>
+ <xul:vbox flex="1" pack="center">
+ <xul:hbox class="xpinstallItemNameRow" align="center">
+ <xul:label class="xpinstallItemName" xbl:inherits="value=name" crop="right"/>
+ <xul:label class="xpinstallItemSigned" xbl:inherits="value=cert,signed"/>
+ </xul:hbox>
+ <xul:hbox class="xpinstallItemDetailsRow" align="center">
+ <xul:textbox class="xpinstallItemURL" xbl:inherits="value=url" flex="1" readonly="true" crop="right"/>
+ </xul:hbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ <implementation>
+ <property name="name" onset="this.setAttribute('name', val); return val;"
+ onget="return this.getAttribute('name');"/>
+ <property name="cert" onset="this.setAttribute('cert', val); return val;"
+ onget="return this.getAttribute('cert');"/>
+ <property name="signed" onset="this.setAttribute('signed', val); return val;"
+ onget="return this.getAttribute('signed');"/>
+ <property name="url" onset="this.setAttribute('url', val); return val;"
+ onget="return this.getAttribute('url');"/>
+ <property name="icon" onset="this.setAttribute('icon', val); return val;"
+ onget="return this.getAttribute('icon');"/>
+ <property name="type" onset="this.setAttribute('type', val); return val;"
+ onget="return this.getAttribute('type');"/>
+ </implementation>
+ </binding>
+
+</bindings>
+
diff --git a/toolkit/mozapps/extensions/docs/SystemAddons.rst b/toolkit/mozapps/extensions/docs/SystemAddons.rst
new file mode 100644
index 000000000..5f724e9ef
--- /dev/null
+++ b/toolkit/mozapps/extensions/docs/SystemAddons.rst
@@ -0,0 +1,224 @@
+Firefox System Add-on Update Protocol
+=====================================
+This document describes the protocol that Firefox uses when retrieving updates
+for System Add-ons from the automatic update service (AUS, currently `Balrog`_),
+and the expected behavior of Firefox based on the updater service's response.
+
+.. _Balrog: https://wiki.mozilla.org/Balrog
+
+System Add-ons
+--------------
+System add-ons:
+
+* Are add-ons that ship with Firefox, are hidden from the UI, and cannot be
+ disabled
+* Can be updated by Firefox depending on the AUS response to Firefox's update
+ request
+* Are stored in two locations:
+
+ * The **default** set ships with Firefox and is stored in the application
+ directory.
+ * The **update** set is stored in the user’s profile directory. If an add-on
+ is both in the update and default set, the update version gets precedence.
+
+Update Request
+--------------
+To determine what updates to install, Firefox makes an HTTP **GET** request to
+AUS once a day via a URL of the form::
+
+ https://aus5.mozilla.org/update/3/SystemAddons/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/%LOCALE%/%CHANNEL%/%OS_VERSION%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/update.xml
+
+The path segments surrounded by ``%`` symbols are variable fields that Firefox
+fills in with information about itself and the environment it's running in:
+
+``VERSION``
+ Firefox version number
+``BUILD_ID``
+ Build ID
+``BUILD_TARGET``
+ Build target
+``LOCALE``
+ Build locale
+``CHANNEL``
+ Update channel
+``OS_VERSION``
+ OS Version
+``DISTRIBUTION``
+ Firefox Distribution
+``DISTRIBUTION_VERSION``
+ Firefox Distribution version
+
+Update Response
+---------------
+AUS should respond with an XML document that looks something like this:
+
+.. code-block:: xml
+
+ <?xml version="1.0"?>
+ <updates>
+ <addons>
+ <addon id="flyweb@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/flyweb/flyweb@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ <addon id="pocket@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/pocket/pocket@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ </addons>
+ </updates>
+
+* The root element is ``<updates>``, used for all updater responses.
+* The only child of ``<updates>`` is ``<addons>``, which represents a list of
+ system add-ons to update.
+* Within ``<addons>`` are several ``<addon>`` tags, each one corresponding to a
+ system add-on to update.
+
+``<addon>`` tags **must** have the following attributes:
+
+``id``
+ The extension ID
+``URL``
+ URL to a signed XPI of the specified add-on version to download
+``hashFunction``
+ Identifier of the hash function used to generate the hashValue attribute.
+``hashValue``
+ Hash of the XPI file linked from the URL attribute, calculated using the function specified in the hashValue attribute.
+``size``
+ Size (in bytes) of the XPI file linked from the URL attribute.
+``version``
+ Version number of the add-on
+
+Update Behavior
+---------------
+After receiving the update response, Firefox modifies the **update** add-ons
+according to the following algorithm:
+
+1. If the ``<addons>`` tag is empty (``<addons></addons>``) in the response,
+ **remove all system add-on updates**.
+2. If no add-ons were specified in the response (i.e. the ``<addons>`` tag
+ is not present), do nothing and finish.
+3. If the **update** add-on set is equal to the set of add-ons specified in the
+ update response, do nothing and finish.
+4. If the set of **default** add-ons is equal to the set of add-ons specified in
+ the update response, remove all the **update** add-ons and finish.
+5. Download each add-on specified in the update response and store them in the
+ "downloaded add-on set". A failed download **must** abort the entire system
+ add-on update.
+6. Validate the downloaded add-ons. The following **must** be true for all
+ downloaded add-ons, or the update process is aborted:
+
+ a. The ID and version of the downloaded add-on must match the specified ID or
+ version in the update response.
+ b. The hash provided in the update response must match the downloaded add-on
+ file.
+ c. The downloaded add-on file size must match the size given in the update
+ response.
+ d. The add-on must be compatible with Firefox (i.e. it must not be for a
+ different application, such as Thunderbird).
+ e. The add-on must be packed (i.e. be an XPI file).
+ f. The add-on must be restartless.
+ g. The add-on must be signed by the system add-on root certificate.
+
+6. Once all downloaded add-ons are validated, install them into the profile
+ directory as part of the **update** set.
+
+Notes on the update process:
+
+* Add-ons are considered "equal" if they have the same ID and version number.
+
+Examples
+--------
+The follow section describes common situations that we have or expect to run
+into and how the protocol described above handles them.
+
+For simplicity, unless otherwise specified, all examples assume that there are
+two system add-ons in existence: **FlyWeb** and **Pocket**.
+
+Basic
+~~~~~
+A user has Firefox 45, which shipped with FlyWeb 1.0 and Pocket 1.0. We want to
+update users to FlyWeb 2.0. AUS sends out the following update response:
+
+.. code-block:: xml
+
+ <updates>
+ <addons>
+ <addon id="flyweb@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/flyweb/flyweb@mozilla.org-2.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="2.0"/>
+ <addon id="pocket@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/pocket/pocket@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ </addons>
+ </updates>
+
+Firefox will download FlyWeb 2.0 and Pocket 1.0 and store them in the profile directory.
+
+Missing Add-on
+~~~~~~~~~~~~~~
+A user has Firefox 45, which shipped with FlyWeb 1.0 and Pocket 1.0. We want to
+update users to FlyWeb 2.0, but accidentally forget to specify Pocket in the
+update response. AUS sends out the following:
+
+.. code-block:: xml
+
+ <updates>
+ <addons>
+ <addon id="flyweb@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/flyweb/flyweb@mozilla.org-2.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="2.0"/>
+ </addons>
+ </updates>
+
+Firefox will download FlyWeb 2.0 and store it in the profile directory. Pocket
+1.0 from the **default** location will be used.
+
+Remove all system add-on updates
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A response from AUS with an empty add-on set will *remove all system add-on
+updates*:
+
+.. code-block:: xml
+
+ <updates>
+ <addons></addons>
+ </updates>
+
+Rollout
+~~~~~~~
+A user has Firefox 45, which shipped with FlyWeb 1.0 and Pocket 1.0. We want to
+rollout FlyWeb 2.0 at a 10% sample rate. 10% of the time, AUS sends out:
+
+.. code-block:: xml
+
+ <updates>
+ <addons>
+ <addon id="flyweb@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/flyweb/flyweb@mozilla.org-2.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="2.0"/>
+ <addon id="pocket@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/pocket/pocket@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ </addons>
+ </updates>
+
+With this response, Firefox will download Pocket 1.0 and FlyWeb 2.0 and install
+them into the profile directory.
+
+The other 90% of the time, AUS sends out an empty response:
+
+.. code-block:: xml
+
+ <updates></updates>
+
+With the empty response, Firefox will not make any changes. This means users who
+haven’t seen the 10% update response will stay on FlyWeb 1.0, and users who have
+seen it will stay on FlyWeb 2.0.
+
+Once we’re happy with the rollout and want to switch to 100%, AUS will send the
+10% update response to 100% of users, upgrading everyone to FlyWeb 2.0.
+
+Rollback
+~~~~~~~~
+This example continues from the “Rollout†example. If, during the 10% rollout,
+we find a major issue with FlyWeb 2.0, we want to roll all users back to FlyWeb 1.0.
+AUS sends out the following:
+
+.. code-block:: xml
+
+ <updates>
+ <addons>
+ <addon id="flyweb@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/flyweb/flyweb@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ <addon id="pocket@mozilla.org" URL="https://ftp.mozilla.org/pub/system-addons/pocket/pocket@mozilla.org-1.0.xpi" hashFunction="sha512" hashValue="abcdef123" size="1234" version="1.0"/>
+ </addons>
+ </updates>
+
+For users who have updated, Firefox will download FlyWeb 1.0 and Pocket 1.0 and
+install them into the profile directory. For users that haven’t yet updated,
+Firefox will see that the **default** add-on set matches the set in the update
+ping and clear the **update** add-on set.
diff --git a/toolkit/mozapps/extensions/docs/index.rst b/toolkit/mozapps/extensions/docs/index.rst
new file mode 100644
index 000000000..ab6ed3c70
--- /dev/null
+++ b/toolkit/mozapps/extensions/docs/index.rst
@@ -0,0 +1,14 @@
+==============
+Add-on Manager
+==============
+
+This is the nascent documentation of the Add-on Manager code.
+
+The public Add-on Manager interfaces are documented on MDN:
+
+https://developer.mozilla.org/en-US/Add-ons/Add-on_Manager
+
+.. toctree::
+ :maxdepth: 1
+
+ SystemAddons
diff --git a/toolkit/mozapps/extensions/extensions.manifest b/toolkit/mozapps/extensions/extensions.manifest
new file mode 100644
index 000000000..c7d2ee386
--- /dev/null
+++ b/toolkit/mozapps/extensions/extensions.manifest
@@ -0,0 +1,27 @@
+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
+#ifndef MOZ_WIDGET_GONK
+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
+#endif
+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/extensions/internal/APIExtensionBootstrap.js b/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
new file mode 100644
index 000000000..0eae2475c
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/APIExtensionBootstrap.js
@@ -0,0 +1,39 @@
+/* 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/extensions/internal/AddonConstants.jsm b/toolkit/mozapps/extensions/internal/AddonConstants.jsm
new file mode 100644
index 000000000..22d91fdf5
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonConstants.jsm
@@ -0,0 +1,31 @@
+/* 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/extensions/internal/AddonLogging.jsm b/toolkit/mozapps/extensions/internal/AddonLogging.jsm
new file mode 100644
index 000000000..f05a6fe6c
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonLogging.jsm
@@ -0,0 +1,192 @@
+/* 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 KEY_PROFILEDIR = "ProfD";
+const FILE_EXTENSIONS_LOG = "extensions.log";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+
+const LOGGER_FILE_PERM = parseInt("666", 8);
+
+const NS_PREFBRANCH_PREFCHANGE_TOPIC_ID = "nsPref:changed";
+
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "LogManager" ];
+
+var gDebugLogEnabled = false;
+
+function formatLogMessage(aType, aName, aStr, aException) {
+ let message = aType.toUpperCase() + " " + aName + ": " + aStr;
+ if (aException) {
+ if (typeof aException == "number")
+ return message + ": " + Components.Exception("", aException).name;
+
+ message = message + ": " + aException;
+ // instanceOf doesn't work here, let's duck type
+ if (aException.fileName)
+ message = message + " (" + aException.fileName + ":" + aException.lineNumber + ")";
+
+ if (aException.message == "too much recursion")
+ dump(message + "\n" + aException.stack + "\n");
+ }
+ return message;
+}
+
+function getStackDetails(aException) {
+ // Defensively wrap all this to ensure that failing to get the message source
+ // doesn't stop the message from being logged
+ try {
+ if (aException) {
+ if (aException instanceof Ci.nsIException) {
+ return {
+ sourceName: aException.filename,
+ lineNumber: aException.lineNumber
+ };
+ }
+
+ if (typeof aException == "object") {
+ return {
+ sourceName: aException.fileName,
+ lineNumber: aException.lineNumber
+ };
+ }
+ }
+
+ let stackFrame = Components.stack.caller.caller.caller;
+ return {
+ sourceName: stackFrame.filename,
+ lineNumber: stackFrame.lineNumber
+ };
+ }
+ catch (e) {
+ return {
+ sourceName: null,
+ lineNumber: 0
+ };
+ }
+}
+
+function AddonLogger(aName) {
+ this.name = aName;
+}
+
+AddonLogger.prototype = {
+ name: null,
+
+ error: function(aStr, aException) {
+ let message = formatLogMessage("error", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.errorFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ // Always dump errors, in case the Console Service isn't listening yet
+ dump("*** " + message + "\n");
+
+ function formatTimestamp(date) {
+ // Format timestamp as: "%Y-%m-%d %H:%M:%S"
+ let year = String(date.getFullYear());
+ let month = String(date.getMonth() + 1).padStart(2, "0");
+ let day = String(date.getDate()).padStart(2, "0");
+ let hours = String(date.getHours()).padStart(2, "0");
+ let minutes = String(date.getMinutes()).padStart(2, "0");
+ let seconds = String(date.getSeconds()).padStart(2, "0");
+
+ return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+ }
+
+ try {
+ var tstamp = new Date();
+ var logfile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_EXTENSIONS_LOG]);
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(logfile, 0x02 | 0x08 | 0x10, LOGGER_FILE_PERM, 0); // write, create, append
+ var writer = Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(Ci.nsIConverterOutputStream);
+ writer.init(stream, "UTF-8", 0, 0x0000);
+ writer.writeString(formatTimestamp(tstamp) + " " +
+ message + " at " + stack.sourceName + ":" +
+ stack.lineNumber + "\n");
+ writer.close();
+ }
+ catch (e) { }
+ },
+
+ warn: function(aStr, aException) {
+ let message = formatLogMessage("warn", this.name, aStr, aException);
+
+ let stack = getStackDetails(aException);
+
+ let consoleMessage = Cc["@mozilla.org/scripterror;1"].
+ createInstance(Ci.nsIScriptError);
+ consoleMessage.init(message, stack.sourceName, null, stack.lineNumber, 0,
+ Ci.nsIScriptError.warningFlag, "component javascript");
+ Services.console.logMessage(consoleMessage);
+
+ if (gDebugLogEnabled)
+ dump("*** " + message + "\n");
+ },
+
+ log: function(aStr, aException) {
+ if (gDebugLogEnabled) {
+ let message = formatLogMessage("log", this.name, aStr, aException);
+ dump("*** " + message + "\n");
+ Services.console.logStringMessage(message);
+ }
+ }
+};
+
+this.LogManager = {
+ getLogger: function(aName, aTarget) {
+ let logger = new AddonLogger(aName);
+
+ if (aTarget) {
+ ["error", "warn", "log"].forEach(function(name) {
+ let fname = name.toUpperCase();
+ delete aTarget[fname];
+ aTarget[fname] = function(aStr, aException) {
+ logger[name](aStr, aException);
+ };
+ });
+ }
+
+ return logger;
+ }
+};
+
+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) {
+ try {
+ gDebugLogEnabled = Services.prefs.getBoolPref(PREF_LOGGING_ENABLED);
+ }
+ catch (e) {
+ gDebugLogEnabled = false;
+ }
+ }
+ }
+};
+
+PrefObserver.init();
diff --git a/toolkit/mozapps/extensions/internal/AddonRepository.jsm b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
new file mode 100644
index 000000000..7f88d44ad
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonRepository.jsm
@@ -0,0 +1,1988 @@
+/* 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/extensions/internal/AddonRepository_SQLiteMigrator.jsm b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
new file mode 100644
index 000000000..e3479643b
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonRepository_SQLiteMigrator.jsm
@@ -0,0 +1,522 @@
+/* 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/extensions/internal/AddonTestUtils.jsm b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
new file mode 100644
index 000000000..bbfb56ad5
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonTestUtils.jsm
@@ -0,0 +1,1232 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* eslint "mozilla/no-aArgs": 1 */
+/* eslint "no-unused-vars": [2, {"args": "none", "varsIgnorePattern": "^(Cc|Ci|Cr|Cu|EXPORTED_SYMBOLS)$"}] */
+/* eslint "semi": [2, "always"] */
+/* eslint "valid-jsdoc": [2, {requireReturn: false}] */
+
+var EXPORTED_SYMBOLS = ["AddonTestUtils", "MockAsyncShutdown"];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
+const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+
+
+Cu.importGlobalProperties(["fetch", "TextEncoder"]);
+
+Cu.import("resource://gre/modules/AsyncShutdown.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const {EventEmitter} = Cu.import("resource://devtools/shared/event-emitter.js", {});
+const {OS} = Cu.import("resource://gre/modules/osfile.jsm", {});
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+
+XPCOMUtils.defineLazyServiceGetter(this, "rdfService",
+ "@mozilla.org/rdf/rdf-service;1", "nsIRDFService");
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+ "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
+
+
+XPCOMUtils.defineLazyGetter(this, "AppInfo", () => {
+ let AppInfo = {};
+ Cu.import("resource://testing-common/AppInfo.jsm", AppInfo);
+ return AppInfo;
+});
+
+
+const ArrayBufferInputStream = Components.Constructor(
+ "@mozilla.org/io/arraybuffer-input-stream;1",
+ "nsIArrayBufferInputStream", "setData");
+
+const nsFile = Components.Constructor(
+ "@mozilla.org/file/local;1",
+ "nsIFile", "initWithPath");
+
+const RDFXMLParser = Components.Constructor(
+ "@mozilla.org/rdf/xml-parser;1",
+ "nsIRDFXMLParser", "parseString");
+
+const RDFDataSource = Components.Constructor(
+ "@mozilla.org/rdf/datasource;1?name=in-memory-datasource",
+ "nsIRDFDataSource");
+
+const ZipReader = Components.Constructor(
+ "@mozilla.org/libjar/zip-reader;1",
+ "nsIZipReader", "open");
+
+const ZipWriter = Components.Constructor(
+ "@mozilla.org/zipwriter;1",
+ "nsIZipWriter", "open");
+
+
+// We need some internal bits of AddonManager
+var AMscope = Cu.import("resource://gre/modules/AddonManager.jsm", {});
+var {AddonManager, AddonManagerPrivate} = AMscope;
+
+
+// Mock out AddonManager's reference to the AsyncShutdown module so we can shut
+// down AddonManager from the test
+var MockAsyncShutdown = {
+ hook: null,
+ status: null,
+ profileBeforeChange: {
+ addBlocker: function(name, blocker, options) {
+ MockAsyncShutdown.hook = blocker;
+ MockAsyncShutdown.status = options.fetchState;
+ }
+ },
+ // We can use the real Barrier
+ Barrier: AsyncShutdown.Barrier,
+};
+
+AMscope.AsyncShutdown = MockAsyncShutdown;
+
+
+/**
+ * Escapes any occurances of &, ", < or > with XML entities.
+ *
+ * @param {string} str
+ * The string to escape.
+ * @return {string} The escaped string.
+ */
+function escapeXML(str) {
+ let replacements = {"&": "&amp;", '"': "&quot;", "'": "&apos;", "<": "&lt;", ">": "&gt;"};
+ return String(str).replace(/[&"''<>]/g, m => replacements[m]);
+}
+
+/**
+ * A tagged template function which escapes any XML metacharacters in
+ * interpolated values.
+ *
+ * @param {Array<string>} strings
+ * An array of literal strings extracted from the templates.
+ * @param {Array} values
+ * An array of interpolated values extracted from the template.
+ * @returns {string}
+ * The result of the escaped values interpolated with the literal
+ * strings.
+ */
+function escaped(strings, ...values) {
+ let result = [];
+
+ for (let [i, string] of strings.entries()) {
+ result.push(string);
+ if (i < values.length)
+ result.push(escapeXML(values[i]));
+ }
+
+ return result.join("");
+}
+
+
+class AddonsList {
+ constructor(extensionsINI) {
+ this.multiprocessIncompatibleIDs = new Set();
+
+ if (!extensionsINI.exists()) {
+ this.extensions = [];
+ this.themes = [];
+ return;
+ }
+
+ let factory = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory);
+
+ let parser = factory.createINIParser(extensionsINI);
+
+ function readDirectories(section) {
+ var dirs = [];
+ var keys = parser.getKeys(section);
+ for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
+ let descriptor = parser.getString(section, key);
+
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ try {
+ file.persistentDescriptor = descriptor;
+ } catch (e) {
+ // Throws if the directory doesn't exist, we can ignore this since the
+ // platform will too.
+ continue;
+ }
+ dirs.push(file);
+ }
+ return dirs;
+ }
+
+ this.extensions = readDirectories("ExtensionDirs");
+ this.themes = readDirectories("ThemeDirs");
+
+ var keys = parser.getKeys("MultiprocessIncompatibleExtensions");
+ for (let key of XPCOMUtils.IterStringEnumerator(keys)) {
+ let id = parser.getString("MultiprocessIncompatibleExtensions", key);
+ this.multiprocessIncompatibleIDs.add(id);
+ }
+ }
+
+ hasItem(type, dir, id) {
+ var path = dir.clone();
+ path.append(id);
+
+ var xpiPath = dir.clone();
+ xpiPath.append(`${id}.xpi`);
+
+ return this[type].some(file => {
+ if (!file.exists())
+ throw new Error(`Non-existent path found in extensions.ini: ${file.path}`);
+
+ if (file.isDirectory())
+ return file.equals(path);
+ if (file.isFile())
+ return file.equals(xpiPath);
+ return false;
+ });
+ }
+
+ isMultiprocessIncompatible(id) {
+ return this.multiprocessIncompatibleIDs.has(id);
+ }
+
+ hasTheme(dir, id) {
+ return this.hasItem("themes", dir, id);
+ }
+
+ hasExtension(dir, id) {
+ return this.hasItem("extensions", dir, id);
+ }
+}
+
+var AddonTestUtils = {
+ addonIntegrationService: null,
+ addonsList: null,
+ appInfo: null,
+ extensionsINI: null,
+ testUnpacked: false,
+ useRealCertChecks: false,
+
+ init(testScope) {
+ this.testScope = testScope;
+
+ // Get the profile directory for tests to use.
+ this.profileDir = testScope.do_get_profile();
+
+ this.extensionsINI = this.profileDir.clone();
+ this.extensionsINI.append("extensions.ini");
+
+ // Register a temporary directory for the tests.
+ this.tempDir = this.profileDir.clone();
+ this.tempDir.append("temp");
+ this.tempDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ this.registerDirectory("TmpD", this.tempDir);
+
+ // Create a replacement app directory for the tests.
+ const appDirForAddons = this.profileDir.clone();
+ appDirForAddons.append("appdir-addons");
+ appDirForAddons.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ this.registerDirectory("XREAddonAppDir", appDirForAddons);
+
+
+ // Enable more extensive EM logging
+ Services.prefs.setBoolPref("extensions.logging.enabled", true);
+
+ // By default only load extensions from the profile install location
+ Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_PROFILE);
+
+ // By default don't disable add-ons from any scope
+ Services.prefs.setIntPref("extensions.autoDisableScopes", 0);
+
+ // By default, don't cache add-ons in AddonRepository.jsm
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
+
+ // Disable the compatibility updates window by default
+ Services.prefs.setBoolPref("extensions.showMismatchUI", false);
+
+ // Point update checks to the local machine for fast failures
+ Services.prefs.setCharPref("extensions.update.url", "http://127.0.0.1/updateURL");
+ Services.prefs.setCharPref("extensions.update.background.url", "http://127.0.0.1/updateBackgroundURL");
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://127.0.0.1/blocklistURL");
+ Services.prefs.setCharPref("services.settings.server", "http://localhost/dummy-kinto/v1");
+
+ // By default ignore bundled add-ons
+ Services.prefs.setBoolPref("extensions.installDistroAddons", false);
+
+ // By default don't check for hotfixes
+ Services.prefs.setCharPref("extensions.hotfix.id", "");
+
+ // Ensure signature checks are enabled by default
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+
+
+ // Write out an empty blocklist.xml file to the profile to ensure nothing
+ // is blocklisted by default
+ var blockFile = OS.Path.join(this.profileDir.path, "blocklist.xml");
+
+ var data = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
+ "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">\n" +
+ "</blocklist>\n";
+
+ this.awaitPromise(OS.File.writeAtomic(blockFile, new TextEncoder().encode(data)));
+
+
+ // Make sure that a given path does not exist
+ function pathShouldntExist(file) {
+ if (file.exists()) {
+ throw new Error(`Test cleanup: path ${file.path} exists when it should not`);
+ }
+ }
+
+ testScope.do_register_cleanup(() => {
+ for (let file of this.tempXPIs) {
+ if (file.exists())
+ file.remove(false);
+ }
+
+ // Check that the temporary directory is empty
+ var dirEntries = this.tempDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ var entries = [];
+ while (dirEntries.hasMoreElements())
+ entries.push(dirEntries.nextFile.leafName);
+ if (entries.length)
+ throw new Error(`Found unexpected files in temporary directory: ${entries.join(", ")}`);
+
+ dirEntries.close();
+
+ try {
+ appDirForAddons.remove(true);
+ } catch (ex) {
+ testScope.do_print(`Got exception removing addon app dir: ${ex}`);
+ }
+
+ // ensure no leftover files in the system addon upgrade location
+ let featuresDir = this.profileDir.clone();
+ featuresDir.append("features");
+ // upgrade directories will be in UUID folders under features/
+ let systemAddonDirs = [];
+ if (featuresDir.exists()) {
+ let featuresDirEntries = featuresDir.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (featuresDirEntries.hasMoreElements()) {
+ let entry = featuresDirEntries.getNext();
+ entry.QueryInterface(Components.interfaces.nsIFile);
+ systemAddonDirs.push(entry);
+ }
+
+ systemAddonDirs.map(dir => {
+ dir.append("stage");
+ pathShouldntExist(dir);
+ });
+ }
+
+ // ensure no leftover files in the user addon location
+ let testDir = this.profileDir.clone();
+ testDir.append("extensions");
+ testDir.append("trash");
+ pathShouldntExist(testDir);
+
+ testDir.leafName = "staged";
+ pathShouldntExist(testDir);
+
+ return this.promiseShutdownManager();
+ });
+ },
+
+ /**
+ * Helper to spin the event loop until a promise resolves or rejects
+ *
+ * @param {Promise} promise
+ * The promise to wait on.
+ * @returns {*} The promise's resolution value.
+ * @throws The promise's rejection value, if it rejects.
+ */
+ awaitPromise(promise) {
+ let done = false;
+ let result;
+ let error;
+ promise.then(
+ val => { result = val; },
+ err => { error = err; }
+ ).then(() => {
+ done = true;
+ });
+
+ while (!done)
+ Services.tm.mainThread.processNextEvent(true);
+
+ if (error !== undefined)
+ throw error;
+ return result;
+ },
+
+ createAppInfo(ID, name, version, platformVersion = "1.0") {
+ AppInfo.updateAppInfo({
+ ID, name, version, platformVersion,
+ crashReporter: true,
+ extraProps: {
+ browserTabsRemoteAutostart: false,
+ },
+ });
+ this.appInfo = AppInfo.getAppInfo();
+ },
+
+ getManifestURI(file) {
+ if (file.isDirectory()) {
+ file.append("install.rdf");
+ if (file.exists()) {
+ return NetUtil.newURI(file);
+ }
+
+ file.leafName = "manifest.json";
+ if (file.exists())
+ return NetUtil.newURI(file);
+
+ throw new Error("No manifest file present");
+ }
+
+ let zip = ZipReader(file);
+ try {
+ let uri = NetUtil.newURI(file);
+
+ if (zip.hasEntry("install.rdf")) {
+ return NetUtil.newURI(`jar:${uri.spec}!/install.rdf`);
+ }
+
+ if (zip.hasEntry("manifest.json")) {
+ return NetUtil.newURI(`jar:${uri.spec}!/manifest.json`);
+ }
+
+ throw new Error("No manifest file present");
+ } finally {
+ zip.close();
+ }
+ },
+
+ getIDFromManifest: Task.async(function*(manifestURI) {
+ let body = yield fetch(manifestURI.spec);
+
+ if (manifestURI.spec.endsWith(".rdf")) {
+ let data = yield body.text();
+
+ let ds = new RDFDataSource();
+ new RDFXMLParser(ds, manifestURI, data);
+
+ let rdfID = ds.GetTarget(rdfService.GetResource("urn:mozilla:install-manifest"),
+ rdfService.GetResource("http://www.mozilla.org/2004/em-rdf#id"),
+ true);
+ return rdfID.QueryInterface(Ci.nsIRDFLiteral).Value;
+ }
+
+ let manifest = yield body.json();
+ try {
+ return manifest.applications.gecko.id;
+ } catch (e) {
+ // IDs for WebExtensions are extracted from the certificate when
+ // not present in the manifest, so just generate a random one.
+ return uuidGen.generateUUID().number;
+ }
+ }),
+
+ overrideCertDB() {
+ // Unregister the real database. This only works because the add-ons manager
+ // hasn't started up and grabbed the certificate database yet.
+ let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+ let factory = registrar.getClassObject(CERTDB_CID, Ci.nsIFactory);
+ registrar.unregisterFactory(CERTDB_CID, factory);
+
+ // Get the real DB
+ let realCertDB = factory.createInstance(null, Ci.nsIX509CertDB);
+
+
+ let verifyCert = Task.async(function*(file, result, cert, callback) {
+ if (result == Cr.NS_ERROR_SIGNED_JAR_NOT_SIGNED &&
+ !this.useRealCertChecks && callback.wrappedJSObject) {
+ // Bypassing XPConnect allows us to create a fake x509 certificate from JS
+ callback = callback.wrappedJSObject;
+
+ try {
+ let manifestURI = this.getManifestURI(file);
+
+ let id = yield this.getIDFromManifest(manifestURI);
+
+ let fakeCert = {commonName: id};
+
+ return [callback, Cr.NS_OK, fakeCert];
+ } catch (e) {
+ // If there is any error then just pass along the original results
+ } finally {
+ // Make sure to close the open zip file or it will be locked.
+ if (file.isFile())
+ Services.obs.notifyObservers(file, "flush-cache-entry", "cert-override");
+ }
+ }
+
+ return [callback, result, cert];
+ }).bind(this);
+
+
+ function FakeCertDB() {
+ for (let property of Object.keys(realCertDB)) {
+ if (property in this)
+ continue;
+
+ if (typeof realCertDB[property] == "function")
+ this[property] = realCertDB[property].bind(realCertDB);
+ }
+ }
+ FakeCertDB.prototype = {
+ openSignedAppFileAsync(root, file, callback) {
+ // First try calling the real cert DB
+ realCertDB.openSignedAppFileAsync(root, file, (result, zipReader, cert) => {
+ verifyCert(file.clone(), result, cert, callback)
+ .then(([callback, result, cert]) => {
+ callback.openSignedAppFileFinished(result, zipReader, cert);
+ });
+ });
+ },
+
+ verifySignedDirectoryAsync(root, dir, callback) {
+ // First try calling the real cert DB
+ realCertDB.verifySignedDirectoryAsync(root, dir, (result, cert) => {
+ verifyCert(dir.clone(), result, cert, callback)
+ .then(([callback, result, cert]) => {
+ callback.verifySignedDirectoryFinished(result, cert);
+ });
+ });
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIX509CertDB]),
+ };
+
+ let certDBFactory = XPCOMUtils.generateSingletonFactory(FakeCertDB);
+ registrar.registerFactory(CERTDB_CID, "CertDB",
+ CERTDB_CONTRACTID, certDBFactory);
+ },
+
+ /**
+ * Starts up the add-on manager as if it was started by the application.
+ *
+ * @param {boolean} [appChanged = true]
+ * An optional boolean parameter to simulate the case where the
+ * application has changed version since the last run. If not passed it
+ * defaults to true
+ * @returns {Promise}
+ * Resolves when the add-on manager's startup has completed.
+ */
+ promiseStartupManager(appChanged = true) {
+ if (this.addonIntegrationService)
+ throw new Error("Attempting to startup manager that was already started.");
+
+ if (appChanged && this.extensionsINI.exists())
+ this.extensionsINI.remove(true);
+
+ this.addonIntegrationService = Cc["@mozilla.org/addons/integration;1"]
+ .getService(Ci.nsIObserver);
+
+ this.addonIntegrationService.observe(null, "addons-startup", null);
+
+ this.emit("addon-manager-started");
+
+ // Load the add-ons list as it was after extension registration
+ this.loadAddonsList();
+
+ return Promise.resolve();
+ },
+
+ promiseShutdownManager() {
+ if (!this.addonIntegrationService)
+ return Promise.resolve(false);
+
+ Services.obs.notifyObservers(null, "quit-application-granted", null);
+ return MockAsyncShutdown.hook()
+ .then(() => {
+ this.emit("addon-manager-shutdown");
+
+ this.addonIntegrationService = null;
+
+ // Load the add-ons list as it was after application shutdown
+ this.loadAddonsList();
+
+ // Clear any crash report annotations
+ this.appInfo.annotations = {};
+
+ // Force the XPIProvider provider to reload to better
+ // simulate real-world usage.
+ let XPIscope = Cu.import("resource://gre/modules/addons/XPIProvider.jsm");
+ // This would be cleaner if I could get it as the rejection reason from
+ // the AddonManagerInternal.shutdown() promise
+ let shutdownError = XPIscope.XPIProvider._shutdownError;
+
+ AddonManagerPrivate.unregisterProvider(XPIscope.XPIProvider);
+ Cu.unload("resource://gre/modules/addons/XPIProvider.jsm");
+
+ if (shutdownError)
+ throw shutdownError;
+
+ return true;
+ });
+ },
+
+ promiseRestartManager(newVersion) {
+ return this.promiseShutdownManager()
+ .then(() => {
+ if (newVersion)
+ this.appInfo.version = newVersion;
+
+ return this.promiseStartupManager(!!newVersion);
+ });
+ },
+
+ loadAddonsList() {
+ this.addonsList = new AddonsList(this.extensionsINI);
+ },
+
+ /**
+ * Creates an update.rdf structure as a string using for the update data passed.
+ *
+ * @param {Object} data
+ * The update data as a JS object. Each property name is an add-on ID,
+ * the property value is an array of each version of the add-on. Each
+ * array value is a JS object containing the data for the version, at
+ * minimum a "version" and "targetApplications" property should be
+ * included to create a functional update manifest.
+ * @return {string} The update.rdf structure as a string.
+ */
+ createUpdateRDF(data) {
+ var rdf = '<?xml version="1.0"?>\n';
+ rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+ ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+
+ for (let addon in data) {
+ rdf += escaped` <Description about="urn:mozilla:extension:${addon}"><em:updates><Seq>\n`;
+
+ for (let versionData of data[addon]) {
+ rdf += ' <li><Description>\n';
+ rdf += this._writeProps(versionData, ["version", "multiprocessCompatible"],
+ ` `);
+ for (let app of versionData.targetApplications || []) {
+ rdf += " <em:targetApplication><Description>\n";
+ rdf += this._writeProps(app, ["id", "minVersion", "maxVersion", "updateLink", "updateHash"],
+ ` `);
+ rdf += " </Description></em:targetApplication>\n";
+ }
+ rdf += ' </Description></li>\n';
+ }
+ rdf += ' </Seq></em:updates></Description>\n';
+ }
+ rdf += "</RDF>\n";
+
+ return rdf;
+ },
+
+ _writeProps(obj, props, indent = " ") {
+ let items = [];
+ for (let prop of props) {
+ if (prop in obj)
+ items.push(escaped`${indent}<em:${prop}>${obj[prop]}</em:${prop}>\n`);
+ }
+ return items.join("");
+ },
+
+ _writeArrayProps(obj, props, indent = " ") {
+ let items = [];
+ for (let prop of props) {
+ for (let val of obj[prop] || [])
+ items.push(escaped`${indent}<em:${prop}>${val}</em:${prop}>\n`);
+ }
+ return items.join("");
+ },
+
+ _writeLocaleStrings(data) {
+ let items = [];
+
+ items.push(this._writeProps(data, ["name", "description", "creator", "homepageURL"]));
+ items.push(this._writeArrayProps(data, ["developer", "translator", "contributor"]));
+
+ return items.join("");
+ },
+
+ createInstallRDF(data) {
+ var rdf = '<?xml version="1.0"?>\n';
+ rdf += '<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"\n' +
+ ' xmlns:em="http://www.mozilla.org/2004/em-rdf#">\n';
+
+ rdf += '<Description about="urn:mozilla:install-manifest">\n';
+
+ let props = ["id", "version", "type", "internalName", "updateURL", "updateKey",
+ "optionsURL", "optionsType", "aboutURL", "iconURL", "icon64URL",
+ "skinnable", "bootstrap", "unpack", "strictCompatibility",
+ "multiprocessCompatible", "hasEmbeddedWebExtension"];
+ rdf += this._writeProps(data, props);
+
+ rdf += this._writeLocaleStrings(data);
+
+ for (let platform of data.targetPlatforms || [])
+ rdf += escaped`<em:targetPlatform>${platform}</em:targetPlatform>\n`;
+
+ for (let app of data.targetApplications || []) {
+ rdf += "<em:targetApplication><Description>\n";
+ rdf += this._writeProps(app, ["id", "minVersion", "maxVersion"]);
+ rdf += "</Description></em:targetApplication>\n";
+ }
+
+ for (let localized of data.localized || []) {
+ rdf += "<em:localized><Description>\n";
+ rdf += this._writeArrayProps(localized, ["locale"]);
+ rdf += this._writeLocaleStrings(localized);
+ rdf += "</Description></em:localized>\n";
+ }
+
+ for (let dep of data.dependencies || [])
+ rdf += escaped`<em:dependency><Description em:id="${dep}"/></em:dependency>\n`;
+
+ rdf += "</Description>\n</RDF>\n";
+ return rdf;
+ },
+
+ /**
+ * Recursively create all directories upto and including the given
+ * path, if they do not exist.
+ *
+ * @param {string} path The path of the directory to create.
+ * @returns {Promise} Resolves when all directories have been created.
+ */
+ recursiveMakeDir(path) {
+ let paths = [];
+ for (let lastPath; path != lastPath; lastPath = path, path = OS.Path.dirname(path))
+ paths.push(path);
+
+ return Promise.all(paths.reverse().map(path =>
+ OS.File.makeDir(path, {ignoreExisting: true}).catch(() => {})));
+ },
+
+ /**
+ * Writes the given data to a file in the given zip file.
+ *
+ * @param {string|nsIFile} zipFile
+ * The zip file to write to.
+ * @param {Object} files
+ * An object containing filenames and the data to write to the
+ * corresponding paths in the zip file.
+ * @param {integer} [flags = 0]
+ * Additional flags to open the file with.
+ */
+ writeFilesToZip(zipFile, files, flags = 0) {
+ if (typeof zipFile == "string")
+ zipFile = nsFile(zipFile);
+
+ var zipW = ZipWriter(zipFile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | flags);
+
+ for (let [path, data] of Object.entries(files)) {
+ if (!(data instanceof ArrayBuffer))
+ data = new TextEncoder("utf-8").encode(data).buffer;
+
+ let stream = ArrayBufferInputStream(data, 0, data.byteLength);
+
+ // Note these files are being created in the XPI archive with date "0" which is 1970-01-01.
+ zipW.addEntryStream(path, 0, Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ }
+
+ zipW.close();
+ },
+
+ promiseWriteFilesToZip: Task.async(function*(zip, files, flags) {
+ yield this.recursiveMakeDir(OS.Path.dirname(zip));
+
+ this.writeFilesToZip(zip, files, flags);
+
+ return Promise.resolve(nsFile(zip));
+ }),
+
+ promiseWriteFilesToDir: Task.async(function*(dir, files) {
+ yield this.recursiveMakeDir(dir);
+
+ for (let [path, data] of Object.entries(files)) {
+ path = path.split("/");
+ let leafName = path.pop();
+
+ // Create parent directories, if necessary.
+ let dirPath = dir;
+ for (let subDir of path) {
+ dirPath = OS.Path.join(dirPath, subDir);
+ yield OS.Path.makeDir(dirPath, {ignoreExisting: true});
+ }
+
+ if (typeof data == "string")
+ data = new TextEncoder("utf-8").encode(data);
+
+ yield OS.File.writeAtomic(OS.Path.join(dirPath, leafName), data);
+ }
+
+ return nsFile(dir);
+ }),
+
+ promiseWriteFilesToExtension(dir, id, files, unpacked = this.testUnpacked) {
+ if (typeof files["install.rdf"] === "object")
+ files["install.rdf"] = this.createInstallRDF(files["install.rdf"]);
+
+ if (unpacked) {
+ let path = OS.Path.join(dir, id);
+
+ return this.promiseWriteFilesToDir(path, files);
+ }
+
+ let xpi = OS.Path.join(dir, `${id}.xpi`);
+
+ return this.promiseWriteFilesToZip(xpi, files);
+ },
+
+ tempXPIs: [],
+ /**
+ * Creates an XPI file for some manifest data in the temporary directory and
+ * returns the nsIFile for it. The file will be deleted when the test completes.
+ *
+ * @param {object} files
+ * The object holding data about the add-on
+ * @return {nsIFile} A file pointing to the created XPI file
+ */
+ createTempXPIFile(files) {
+ var file = this.tempDir.clone();
+ let uuid = uuidGen.generateUUID().number.slice(1, -1);
+ file.append(`${uuid}.xpi`);
+
+ this.tempXPIs.push(file);
+
+ if (typeof files["install.rdf"] === "object")
+ files["install.rdf"] = this.createInstallRDF(files["install.rdf"]);
+
+ this.writeFilesToZip(file.path, files);
+ return file;
+ },
+
+ /**
+ * Creates an XPI file for some WebExtension data in the temporary directory and
+ * returns the nsIFile for it. The file will be deleted when the test completes.
+ *
+ * @param {Object} data
+ * The object holding data about the add-on, as expected by
+ * |Extension.generateXPI|.
+ * @return {nsIFile} A file pointing to the created XPI file
+ */
+ createTempWebExtensionFile(data) {
+ let file = Extension.generateXPI(data);
+ this.tempXPIs.push(file);
+ return file;
+ },
+
+ /**
+ * Creates an extension proxy file.
+ * See: https://developer.mozilla.org/en-US/Add-ons/Setting_up_extension_development_environment#Firefox_extension_proxy_file
+ *
+ * @param {nsIFile} dir
+ * The directory to add the proxy file to.
+ * @param {nsIFile} addon
+ * An nsIFile for the add-on file that this is a proxy file for.
+ * @param {string} id
+ * A string to use for the add-on ID.
+ * @returns {Promise} Resolves when the file has been created.
+ */
+ promiseWriteProxyFileToDir(dir, addon, id) {
+ let files = {
+ [id]: addon.path,
+ };
+
+ return this.promiseWriteFilesToDir(dir.path, files);
+ },
+
+ /**
+ * Manually installs an XPI file into an install location by either copying the
+ * XPI there or extracting it depending on whether unpacking is being tested
+ * or not.
+ *
+ * @param {nsIFile} xpiFile
+ * The XPI file to install.
+ * @param {nsIFile} installLocation
+ * The install location (an nsIFile) to install into.
+ * @param {string} id
+ * The ID to install as.
+ * @param {boolean} [unpacked = this.testUnpacked]
+ * If true, install as an unpacked directory, rather than a
+ * packed XPI.
+ * @returns {nsIFile}
+ * A file pointing to the installed location of the XPI file or
+ * unpacked directory.
+ */
+ manuallyInstall(xpiFile, installLocation, id, unpacked = this.testUnpacked) {
+ if (unpacked) {
+ let dir = installLocation.clone();
+ dir.append(id);
+ dir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let zip = ZipReader(xpiFile);
+ let entries = zip.findEntries(null);
+ while (entries.hasMore()) {
+ let entry = entries.getNext();
+ let target = dir.clone();
+ for (let part of entry.split("/"))
+ target.append(part);
+ zip.extract(entry, target);
+ }
+ zip.close();
+
+ return dir;
+ }
+
+ let target = installLocation.clone();
+ target.append(`${id}.xpi`);
+ xpiFile.copyTo(target.parent, target.leafName);
+ return target;
+ },
+
+ /**
+ * Manually uninstalls an add-on by removing its files from the install
+ * location.
+ *
+ * @param {nsIFile} installLocation
+ * The nsIFile of the install location to remove from.
+ * @param {string} id
+ * The ID of the add-on to remove.
+ * @param {boolean} [unpacked = this.testUnpacked]
+ * If true, uninstall an unpacked directory, rather than a
+ * packed XPI.
+ */
+ manuallyUninstall(installLocation, id, unpacked = this.testUnpacked) {
+ let file = this.getFileForAddon(installLocation, id, unpacked);
+
+ // In reality because the app is restarted a flush isn't necessary for XPIs
+ // removed outside the app, but for testing we must flush manually.
+ if (file.isFile())
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+
+ file.remove(true);
+ },
+
+ /**
+ * Gets the nsIFile for where an add-on is installed. It may point to a file or
+ * a directory depending on whether add-ons are being installed unpacked or not.
+ *
+ * @param {nsIFile} dir
+ * The nsIFile for the install location
+ * @param {string} id
+ * The ID of the add-on
+ * @param {boolean} [unpacked = this.testUnpacked]
+ * If true, return the path to an unpacked directory, rather than a
+ * packed XPI.
+ * @returns {nsIFile}
+ * A file pointing to the XPI file or unpacked directory where
+ * the add-on should be installed.
+ */
+ getFileForAddon(dir, id, unpacked = this.testUnpacked) {
+ dir = dir.clone();
+ if (unpacked)
+ dir.append(id);
+ else
+ dir.append(`${id}.xpi`);
+ return dir;
+ },
+
+ /**
+ * Sets the last modified time of the extension, usually to trigger an update
+ * of its metadata. If the extension is unpacked, this function assumes that
+ * the extension contains only the install.rdf file.
+ *
+ * @param {nsIFile} ext A file pointing to either the packed extension or its unpacked directory.
+ * @param {number} time The time to which we set the lastModifiedTime of the extension
+ *
+ * @deprecated Please use promiseSetExtensionModifiedTime instead
+ */
+ setExtensionModifiedTime(ext, time) {
+ ext.lastModifiedTime = time;
+ if (ext.isDirectory()) {
+ let entries = ext.directoryEntries
+ .QueryInterface(Ci.nsIDirectoryEnumerator);
+ while (entries.hasMoreElements())
+ this.setExtensionModifiedTime(entries.nextFile, time);
+ entries.close();
+ }
+ },
+
+ promiseSetExtensionModifiedTime: Task.async(function*(path, time) {
+ yield OS.File.setDates(path, time, time);
+
+ let iterator = new OS.File.DirectoryIterator(path);
+ try {
+ yield iterator.forEach(entry => {
+ return this.promiseSetExtensionModifiedTime(entry.path, time);
+ });
+ } catch (ex) {
+ if (ex instanceof OS.File.Error)
+ return;
+ throw ex;
+ } finally {
+ iterator.close().catch(() => {});
+ }
+ }),
+
+ registerDirectory(key, dir) {
+ var dirProvider = {
+ getFile(prop, persistent) {
+ persistent.value = false;
+ if (prop == key)
+ return dir.clone();
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider]),
+ };
+ Services.dirsvc.registerProvider(dirProvider);
+ },
+
+ /**
+ * Returns a promise that resolves when the given add-on event is fired. The
+ * resolved value is an array of arguments passed for the event.
+ *
+ * @param {string} event
+ * The name of the AddonListener event handler method for which
+ * an event is expected.
+ * @returns {Promise<Array>}
+ * Resolves to an array containing the event handler's
+ * arguments the first time it is called.
+ */
+ promiseAddonEvent(event) {
+ return new Promise(resolve => {
+ let listener = {
+ [event](...args) {
+ AddonManager.removeAddonListener(listener);
+ resolve(args);
+ },
+ };
+
+ AddonManager.addAddonListener(listener);
+ });
+ },
+
+ /**
+ * A helper method to install AddonInstall and wait for completion.
+ *
+ * @param {AddonInstall} install
+ * The add-on to install.
+ * @returns {Promise}
+ * Resolves when the install completes, either successfully or
+ * in failure.
+ */
+ promiseCompleteInstall(install) {
+ let listener;
+ return new Promise(resolve => {
+ listener = {
+ onDownloadFailed: resolve,
+ onDownloadCancelled: resolve,
+ onInstallFailed: resolve,
+ onInstallCancelled: resolve,
+ onInstallEnded: resolve,
+ onInstallPostponed: resolve,
+ };
+
+ install.addListener(listener);
+ install.install();
+ }).then(() => {
+ install.removeListener(listener);
+ });
+ },
+
+ /**
+ * A helper method to install a file.
+ *
+ * @param {nsIFile} file
+ * The file to install
+ * @param {boolean} [ignoreIncompatible = false]
+ * Optional parameter to ignore add-ons that are incompatible
+ * with the application
+ * @returns {Promise}
+ * Resolves when the install has completed.
+ */
+ promiseInstallFile(file, ignoreIncompatible = false) {
+ return new Promise((resolve, reject) => {
+ AddonManager.getInstallForFile(file, install => {
+ if (!install)
+ reject(new Error(`No AddonInstall created for ${file.path}`));
+ else if (install.state != AddonManager.STATE_DOWNLOADED)
+ reject(new Error(`Expected file to be downloaded for install of ${file.path}`));
+ else if (ignoreIncompatible && install.addon.appDisabled)
+ resolve();
+ else
+ resolve(this.promiseCompleteInstall(install));
+ });
+ });
+ },
+
+ /**
+ * A helper method to install an array of files.
+ *
+ * @param {Iterable<nsIFile>} files
+ * The files to install
+ * @param {boolean} [ignoreIncompatible = false]
+ * Optional parameter to ignore add-ons that are incompatible
+ * with the application
+ * @returns {Promise}
+ * Resolves when the installs have completed.
+ */
+ promiseInstallAllFiles(files, ignoreIncompatible = false) {
+ return Promise.all(Array.from(
+ files,
+ file => this.promiseInstallFile(file, ignoreIncompatible)));
+ },
+
+ promiseCompleteAllInstalls(installs) {
+ return Promise.all(Array.from(installs, this.promiseCompleteInstall));
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonsByIDs.
+ *
+ * @param {Array<string>} list
+ * As the first argument of AddonManager.getAddonsByIDs
+ * @return {Promise<Array<Addon>>}
+ * Resolves to the array of add-ons for the given IDs.
+ */
+ promiseAddonsByIDs(list) {
+ return new Promise(resolve => AddonManager.getAddonsByIDs(list, resolve));
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonByID.
+ *
+ * @param {string} id
+ * The ID of the add-on.
+ * @return {Promise<Addon>}
+ * Resolves to the add-on with the given ID.
+ */
+ promiseAddonByID(id) {
+ return new Promise(resolve => AddonManager.getAddonByID(id, resolve));
+ },
+
+ /**
+ * Returns a promise that will be resolved when an add-on update check is
+ * complete. The value resolved will be an AddonInstall if a new version was
+ * found.
+ *
+ * @param {object} addon The add-on to find updates for.
+ * @param {integer} reason The type of update to find.
+ * @return {Promise<object>} an object containing information about the update.
+ */
+ promiseFindAddonUpdates(addon, reason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ let equal = this.testScope.equal;
+ return new Promise((resolve, reject) => {
+ let result = {};
+ addon.findUpdates({
+ onNoCompatibilityUpdateAvailable: function(addon2) {
+ if ("compatibilityUpdate" in result) {
+ throw new Error("Saw multiple compatibility update events");
+ }
+ equal(addon, addon2, "onNoCompatibilityUpdateAvailable");
+ result.compatibilityUpdate = false;
+ },
+
+ onCompatibilityUpdateAvailable: function(addon2) {
+ if ("compatibilityUpdate" in result) {
+ throw new Error("Saw multiple compatibility update events");
+ }
+ equal(addon, addon2, "onCompatibilityUpdateAvailable");
+ result.compatibilityUpdate = true;
+ },
+
+ onNoUpdateAvailable: function(addon2) {
+ if ("updateAvailable" in result) {
+ throw new Error("Saw multiple update available events");
+ }
+ equal(addon, addon2, "onNoUpdateAvailable");
+ result.updateAvailable = false;
+ },
+
+ onUpdateAvailable: function(addon2, install) {
+ if ("updateAvailable" in result) {
+ throw new Error("Saw multiple update available events");
+ }
+ equal(addon, addon2, "onUpdateAvailable");
+ result.updateAvailable = install;
+ },
+
+ onUpdateFinished: function(addon2, error) {
+ equal(addon, addon2, "onUpdateFinished");
+ if (error == AddonManager.UPDATE_STATUS_NO_ERROR) {
+ resolve(result);
+ } else {
+ result.error = error;
+ reject(result);
+ }
+ }
+ }, reason);
+ });
+ },
+
+ /**
+ * A promise-based variant of AddonManager.getAddonsWithOperationsByTypes
+ *
+ * @param {Array<string>} types
+ * The first argument to AddonManager.getAddonsWithOperationsByTypes
+ * @return {Promise<Array<Addon>>}
+ * Resolves to an array of add-ons with the given operations
+ * pending.
+ */
+ promiseAddonsWithOperationsByTypes(types) {
+ return new Promise(resolve => AddonManager.getAddonsWithOperationsByTypes(types, resolve));
+ },
+
+ /**
+ * Monitors console output for the duration of a task, and returns a promise
+ * which resolves to a tuple containing a list of all console messages
+ * generated during the task's execution, and the result of the task itself.
+ *
+ * @param {function} task
+ * The task to run while monitoring console output. May be
+ * either a generator function, per Task.jsm, or an ordinary
+ * function which returns promose.
+ * @return {Promise<[Array<nsIConsoleMessage>, *]>}
+ * Resolves to an object containing a `messages` property, with
+ * the array of console messages emitted during the execution
+ * of the task, and a `result` property, containing the task's
+ * return value.
+ */
+ promiseConsoleOutput: Task.async(function*(task) {
+ const DONE = "=== xpcshell test console listener done ===";
+
+ let listener, messages = [];
+ let awaitListener = new Promise(resolve => {
+ listener = msg => {
+ if (msg == DONE) {
+ resolve();
+ } else {
+ msg instanceof Ci.nsIScriptError;
+ 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);
+ }
+ }),
+};
+
+for (let [key, val] of Object.entries(AddonTestUtils)) {
+ if (typeof val == "function")
+ AddonTestUtils[key] = val.bind(AddonTestUtils);
+}
+
+EventEmitter.decorate(AddonTestUtils);
diff --git a/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
new file mode 100644
index 000000000..63c16737c
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/AddonUpdateChecker.jsm
@@ -0,0 +1,934 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * The AddonUpdateChecker is responsible for retrieving the update information
+ * from an add-on's remote update manifest.
+ */
+
+"use strict";
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cu = Components.utils;
+
+this.EXPORTED_SYMBOLS = [ "AddonUpdateChecker" ];
+
+const TIMEOUT = 60 * 1000;
+const PREFIX_NS_RDF = "http://www.w3.org/1999/02/22-rdf-syntax-ns#";
+const PREFIX_NS_EM = "http://www.mozilla.org/2004/em-rdf#";
+const PREFIX_ITEM = "urn:mozilla:item:";
+const PREFIX_EXTENSION = "urn:mozilla:extension:";
+const PREFIX_THEME = "urn:mozilla:theme:";
+const TOOLKIT_ID = "toolkit@mozilla.org"
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+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, "AddonRepository",
+ "resource://gre/modules/addons/AddonRepository.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+
+// Shared code for suppressing bad cert dialogs.
+XPCOMUtils.defineLazyGetter(this, "CertUtils", function() {
+ let certUtils = {};
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", certUtils);
+ return certUtils;
+});
+
+var gRDF = Cc["@mozilla.org/rdf/rdf-service;1"].
+ getService(Ci.nsIRDFService);
+
+Cu.import("resource://gre/modules/Log.jsm");
+const LOGGER_ID = "addons.update-checker";
+
+// Create a new logger for use by the Addons Update Checker
+// (Requires AddonManager.jsm)
+var logger = Log.repository.getLogger(LOGGER_ID);
+
+/**
+ * A serialisation method for RDF data that produces an identical string
+ * for matching RDF graphs.
+ * The serialisation is not complete, only assertions stemming from a given
+ * resource are included, multiple references to the same resource are not
+ * permitted, and the RDF prolog and epilog are not included.
+ * RDF Blob and Date literals are not supported.
+ */
+function RDFSerializer() {
+ this.cUtils = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ this.resources = [];
+}
+
+RDFSerializer.prototype = {
+ INDENT: " ", // The indent used for pretty-printing
+ resources: null, // Array of the resources that have been found
+
+ /**
+ * Escapes characters from a string that should not appear in XML.
+ *
+ * @param aString
+ * The string to be escaped
+ * @return a string with all characters invalid in XML character data
+ * converted to entity references.
+ */
+ escapeEntities: function(aString) {
+ aString = aString.replace(/&/g, "&amp;");
+ aString = aString.replace(/</g, "&lt;");
+ aString = aString.replace(/>/g, "&gt;");
+ return aString.replace(/"/g, "&quot;");
+ },
+
+ /**
+ * Serializes all the elements of an RDF container.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aContainer
+ * The RDF container to output the child elements of
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized elements.
+ */
+ serializeContainerItems: function(aDs, aContainer, aIndent) {
+ var result = "";
+ var items = aContainer.GetElements();
+ while (items.hasMoreElements()) {
+ var item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ result += aIndent + "<RDF:li>\n"
+ result += this.serializeResource(aDs, item, aIndent + this.INDENT);
+ result += aIndent + "</RDF:li>\n"
+ }
+ return result;
+ },
+
+ /**
+ * Serializes all em:* (see EM_NS) properties of an RDF resource except for
+ * the em:signature property. As this serialization is to be compared against
+ * the manifest signature it cannot contain the em:signature property itself.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource that contains the properties to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing
+ * @return a string containing the serialized properties.
+ * @throws if the resource contains a property that cannot be serialized
+ */
+ serializeResourceProperties: function(aDs, aResource, aIndent) {
+ var result = "";
+ var items = [];
+ var arcs = aDs.ArcLabelsOut(aResource);
+ while (arcs.hasMoreElements()) {
+ var arc = arcs.getNext().QueryInterface(Ci.nsIRDFResource);
+ if (arc.ValueUTF8.substring(0, PREFIX_NS_EM.length) != PREFIX_NS_EM)
+ continue;
+ var prop = arc.ValueUTF8.substring(PREFIX_NS_EM.length);
+ if (prop == "signature")
+ continue;
+
+ var targets = aDs.GetTargets(aResource, arc, true);
+ while (targets.hasMoreElements()) {
+ var target = targets.getNext();
+ if (target instanceof Ci.nsIRDFResource) {
+ var item = aIndent + "<em:" + prop + ">\n";
+ item += this.serializeResource(aDs, target, aIndent + this.INDENT);
+ item += aIndent + "</em:" + prop + ">\n";
+ items.push(item);
+ }
+ else if (target instanceof Ci.nsIRDFLiteral) {
+ items.push(aIndent + "<em:" + prop + ">" +
+ this.escapeEntities(target.Value) + "</em:" + prop + ">\n");
+ }
+ else if (target instanceof Ci.nsIRDFInt) {
+ items.push(aIndent + "<em:" + prop + " NC:parseType=\"Integer\">" +
+ target.Value + "</em:" + prop + ">\n");
+ }
+ else {
+ throw Components.Exception("Cannot serialize unknown literal type");
+ }
+ }
+ }
+ items.sort();
+ result += items.join("");
+ return result;
+ },
+
+ /**
+ * Recursively serializes an RDF resource and all resources it links to.
+ * This will only output EM_NS properties and will ignore any em:signature
+ * property.
+ *
+ * @param aDs
+ * The RDF datasource
+ * @param aResource
+ * The RDF resource to serialize
+ * @param aIndent
+ * The current level of indent for pretty-printing. If undefined no
+ * indent will be added
+ * @return a string containing the serialized resource.
+ * @throws if the RDF data contains multiple references to the same resource.
+ */
+ serializeResource: function(aDs, aResource, aIndent) {
+ if (this.resources.indexOf(aResource) != -1 ) {
+ // We cannot output multiple references to the same resource.
+ throw Components.Exception("Cannot serialize multiple references to " + aResource.Value);
+ }
+ if (aIndent === undefined)
+ aIndent = "";
+
+ this.resources.push(aResource);
+ var container = null;
+ var type = "Description";
+ if (this.cUtils.IsSeq(aDs, aResource)) {
+ type = "Seq";
+ container = this.cUtils.MakeSeq(aDs, aResource);
+ }
+ else if (this.cUtils.IsAlt(aDs, aResource)) {
+ type = "Alt";
+ container = this.cUtils.MakeAlt(aDs, aResource);
+ }
+ else if (this.cUtils.IsBag(aDs, aResource)) {
+ type = "Bag";
+ container = this.cUtils.MakeBag(aDs, aResource);
+ }
+
+ var result = aIndent + "<RDF:" + type;
+ if (!gRDF.IsAnonymousResource(aResource))
+ result += " about=\"" + this.escapeEntities(aResource.ValueUTF8) + "\"";
+ result += ">\n";
+
+ if (container)
+ result += this.serializeContainerItems(aDs, container, aIndent + this.INDENT);
+
+ result += this.serializeResourceProperties(aDs, aResource, aIndent + this.INDENT);
+
+ result += aIndent + "</RDF:" + type + ">\n";
+ return result;
+ }
+}
+
+/**
+ * Sanitizes the update URL in an update item, as returned by
+ * parseRDFManifest and parseJSONManifest. Ensures that:
+ *
+ * - The URL is secure, or secured by a strong enough hash.
+ * - The security principal of the update manifest has permission to
+ * load the URL.
+ *
+ * @param aUpdate
+ * The update item to sanitize.
+ * @param aRequest
+ * The XMLHttpRequest used to load the manifest.
+ * @param aHashPattern
+ * The regular expression used to validate the update hash.
+ * @param aHashString
+ * The human-readable string specifying which hash functions
+ * are accepted.
+ */
+function sanitizeUpdateURL(aUpdate, aRequest, aHashPattern, aHashString) {
+ if (aUpdate.updateURL) {
+ let scriptSecurity = Services.scriptSecurityManager;
+ let principal = scriptSecurity.getChannelURIPrincipal(aRequest.channel);
+ try {
+ // This logs an error on failure, so no need to log it a second time
+ scriptSecurity.checkLoadURIStrWithPrincipal(principal, aUpdate.updateURL,
+ scriptSecurity.DISALLOW_SCRIPT);
+ } catch (e) {
+ delete aUpdate.updateURL;
+ return;
+ }
+
+ if (AddonManager.checkUpdateSecurity &&
+ !aUpdate.updateURL.startsWith("https:") &&
+ !aHashPattern.test(aUpdate.updateHash)) {
+ logger.warn(`Update link ${aUpdate.updateURL} is not secure and is not verified ` +
+ `by a strong enough hash (needs to be ${aHashString}).`);
+ delete aUpdate.updateURL;
+ delete aUpdate.updateHash;
+ }
+ }
+}
+
+/**
+ * Parses an RDF style update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a bare XML DOM document
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseRDFManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aManifestData.documentElement.namespaceURI != PREFIX_NS_RDF) {
+ throw Components.Exception("Update manifest had an unrecognised namespace: " +
+ aManifestData.documentElement.namespaceURI);
+ }
+
+ function EM_R(aProp) {
+ return gRDF.GetResource(PREFIX_NS_EM + aProp);
+ }
+
+ function getValue(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;
+ }
+
+ function getProperty(aDs, aSource, aProperty) {
+ return getValue(aDs.GetTarget(aSource, EM_R(aProperty), true));
+ }
+
+ function getBooleanProperty(aDs, aSource, aProperty) {
+ let propValue = aDs.GetTarget(aSource, EM_R(aProperty), true);
+ if (!propValue)
+ return undefined;
+ return getValue(propValue) == "true";
+ }
+
+ function getRequiredProperty(aDs, aSource, aProperty) {
+ let value = getProperty(aDs, aSource, aProperty);
+ if (!value)
+ throw Components.Exception("Update manifest is missing a required " + aProperty + " property.");
+ return value;
+ }
+
+ 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);
+ rdfParser.parseString(ds, aRequest.channel.URI, aRequest.responseText);
+
+ // Differentiating between add-on types is deprecated
+ let extensionRes = gRDF.GetResource(PREFIX_EXTENSION + aId);
+ let themeRes = gRDF.GetResource(PREFIX_THEME + aId);
+ let itemRes = gRDF.GetResource(PREFIX_ITEM + aId);
+ let addonRes;
+ if (ds.ArcLabelsOut(extensionRes).hasMoreElements())
+ addonRes = extensionRes;
+ else if (ds.ArcLabelsOut(themeRes).hasMoreElements())
+ addonRes = themeRes;
+ else
+ addonRes = itemRes;
+
+ // If we have an update key then the update manifest must be signed
+ if (aUpdateKey) {
+ let signature = getProperty(ds, addonRes, "signature");
+ if (!signature)
+ throw Components.Exception("Update manifest for " + aId + " does not contain a required signature");
+ let serializer = new RDFSerializer();
+ let updateString = null;
+
+ try {
+ updateString = serializer.serializeResource(ds, addonRes);
+ }
+ catch (e) {
+ throw Components.Exception("Failed to generate signed string for " + aId + ". Serializer threw " + e,
+ e.result);
+ }
+
+ let result = false;
+
+ try {
+ let verifier = Cc["@mozilla.org/security/datasignatureverifier;1"].
+ getService(Ci.nsIDataSignatureVerifier);
+ result = verifier.verifyData(updateString, signature, aUpdateKey);
+ }
+ catch (e) {
+ throw Components.Exception("The signature or updateKey for " + aId + " is malformed." +
+ "Verifier threw " + e, e.result);
+ }
+
+ if (!result)
+ throw Components.Exception("The signature for " + aId + " was not created by the add-on's updateKey");
+ }
+
+ let updates = ds.GetTarget(addonRes, EM_R("updates"), true);
+
+ // A missing updates property doesn't count as a failure, just as no avialable
+ // update information
+ if (!updates) {
+ logger.warn("Update manifest for " + aId + " did not contain an updates property");
+ return [];
+ }
+
+ if (!(updates instanceof Ci.nsIRDFResource))
+ throw Components.Exception("Missing updates property for " + addonRes.Value);
+
+ let cu = Cc["@mozilla.org/rdf/container-utils;1"].
+ getService(Ci.nsIRDFContainerUtils);
+ if (!cu.IsContainer(ds, updates))
+ throw Components.Exception("Updates property was not an RDF container");
+
+ let results = [];
+ let ctr = Cc["@mozilla.org/rdf/container;1"].
+ createInstance(Ci.nsIRDFContainer);
+ ctr.Init(ds, updates);
+ let items = ctr.GetElements();
+ while (items.hasMoreElements()) {
+ let item = items.getNext().QueryInterface(Ci.nsIRDFResource);
+ let version = getProperty(ds, item, "version");
+ if (!version) {
+ logger.warn("Update manifest is missing a required version property.");
+ continue;
+ }
+
+ logger.debug("Found an update entry for " + aId + " version " + version);
+
+ let targetApps = ds.GetTargets(item, EM_R("targetApplication"), true);
+ while (targetApps.hasMoreElements()) {
+ let targetApp = targetApps.getNext().QueryInterface(Ci.nsIRDFResource);
+
+ let appEntry = {};
+ try {
+ appEntry.id = getRequiredProperty(ds, targetApp, "id");
+ appEntry.minVersion = getRequiredProperty(ds, targetApp, "minVersion");
+ appEntry.maxVersion = getRequiredProperty(ds, targetApp, "maxVersion");
+ }
+ catch (e) {
+ logger.warn(e);
+ continue;
+ }
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getBooleanProperty(ds, item, "multiprocessCompatible"),
+ updateURL: getProperty(ds, targetApp, "updateLink"),
+ updateHash: getProperty(ds, targetApp, "updateHash"),
+ updateInfoURL: getProperty(ds, targetApp, "updateInfoURL"),
+ strictCompatibility: !!getBooleanProperty(ds, targetApp, "strictCompatibility"),
+ targetApplications: [appEntry]
+ };
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha/, "sha1 or stronger");
+
+ results.push(result);
+ }
+ }
+ return results;
+}
+
+/**
+ * Parses an JSON update manifest into an array of update objects.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aRequest
+ * The XMLHttpRequest that has retrieved the update manifest
+ * @param aManifestData
+ * The pre-parsed manifest, as a JSON object tree
+ * @return an array of update objects
+ * @throws if the update manifest is invalid in any way
+ */
+function parseJSONManifest(aId, aUpdateKey, aRequest, aManifestData) {
+ if (aUpdateKey)
+ throw Components.Exception("Update keys are not supported for JSON update manifests");
+
+ let TYPE_CHECK = {
+ "array": val => Array.isArray(val),
+ "object": val => val && typeof val == "object" && !Array.isArray(val),
+ };
+
+ function getProperty(aObj, aProperty, aType, aDefault = undefined) {
+ if (!(aProperty in aObj))
+ return aDefault;
+
+ let value = aObj[aProperty];
+
+ let matchesType = aType in TYPE_CHECK ? TYPE_CHECK[aType](value) : typeof value == aType;
+ if (!matchesType)
+ throw Components.Exception(`Update manifest property '${aProperty}' has incorrect type (expected ${aType})`);
+
+ return value;
+ }
+
+ function getRequiredProperty(aObj, aProperty, aType) {
+ let value = getProperty(aObj, aProperty, aType);
+ if (value === undefined)
+ throw Components.Exception(`Update manifest is missing a required ${aProperty} property.`);
+ return value;
+ }
+
+ let manifest = aManifestData;
+
+ if (!TYPE_CHECK["object"](manifest))
+ throw Components.Exception("Root element of update manifest must be a JSON object literal");
+
+ // The set of add-ons this manifest has updates for
+ let addons = getRequiredProperty(manifest, "addons", "object");
+
+ // The entry for this particular add-on
+ let addon = getProperty(addons, aId, "object");
+
+ // A missing entry doesn't count as a failure, just as no avialable update
+ // information
+ if (!addon) {
+ logger.warn("Update manifest did not contain an entry for " + aId);
+ return [];
+ }
+
+ // The list of available updates
+ let updates = getProperty(addon, "updates", "array", []);
+
+ let results = [];
+
+ for (let update of updates) {
+ let version = getRequiredProperty(update, "version", "string");
+
+ logger.debug(`Found an update entry for ${aId} version ${version}`);
+
+ let applications = getProperty(update, "applications", "object",
+ { gecko: {} });
+
+ // "gecko" is currently the only supported application entry. If
+ // it's missing, skip this update.
+ if (!("gecko" in applications)) {
+ logger.debug("gecko not in application entry, skipping update of ${addon}")
+ continue;
+ }
+
+ let app = getProperty(applications, "gecko", "object");
+
+ let appEntry = {
+ id: TOOLKIT_ID,
+ minVersion: getProperty(app, "strict_min_version", "string",
+ AddonManagerPrivate.webExtensionsMinPlatformVersion),
+ maxVersion: "*",
+ };
+
+ let result = {
+ id: aId,
+ version: version,
+ multiprocessCompatible: getProperty(update, "multiprocess_compatible", "boolean", true),
+ updateURL: getProperty(update, "update_link", "string"),
+ updateHash: getProperty(update, "update_hash", "string"),
+ updateInfoURL: getProperty(update, "update_info_url", "string"),
+ strictCompatibility: false,
+ targetApplications: [appEntry],
+ };
+
+ if ("strict_max_version" in app) {
+ if ("advisory_max_version" in app) {
+ logger.warn("Ignoring 'advisory_max_version' update manifest property for " +
+ aId + " property since 'strict_max_version' also present");
+ }
+
+ appEntry.maxVersion = getProperty(app, "strict_max_version", "string");
+ result.strictCompatibility = appEntry.maxVersion != "*";
+ } else if ("advisory_max_version" in app) {
+ appEntry.maxVersion = getProperty(app, "advisory_max_version", "string");
+ }
+
+ // The JSON update protocol requires an SHA-2 hash. RDF still
+ // supports SHA-1, for compatibility reasons.
+ sanitizeUpdateURL(result, aRequest, /^sha(256|512):/, "sha256 or sha512");
+
+ results.push(result);
+ }
+ return results;
+}
+
+/**
+ * Starts downloading an update manifest and then passes it to an appropriate
+ * parser to convert to an array of update objects
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the update manifest
+ * @param aObserver
+ * An observer to pass results to
+ */
+function UpdateParser(aId, aUpdateKey, aUrl, aObserver) {
+ this.id = aId;
+ this.updateKey = aUpdateKey;
+ this.observer = aObserver;
+ this.url = aUrl;
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ logger.debug("Requesting " + aUrl);
+ try {
+ this.request = new ServiceRequest();
+ this.request.open("GET", this.url, true);
+ this.request.channel.notificationCallbacks = new CertUtils.BadCertHandler(!requireBuiltIn);
+ this.request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to cache.
+ this.request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ this.request.overrideMimeType("text/plain");
+ this.request.setRequestHeader("Moz-XPI-Update", "1", true);
+ this.request.timeout = TIMEOUT;
+ this.request.addEventListener("load", () => this.onLoad(), false);
+ this.request.addEventListener("error", () => this.onError(), false);
+ this.request.addEventListener("timeout", () => this.onTimeout(), false);
+ this.request.send(null);
+ }
+ catch (e) {
+ logger.error("Failed to request update manifest", e);
+ }
+}
+
+UpdateParser.prototype = {
+ id: null,
+ updateKey: null,
+ observer: null,
+ request: null,
+ url: null,
+
+ /**
+ * Called when the manifest has been successfully loaded.
+ */
+ onLoad: function() {
+ let request = this.request;
+ this.request = null;
+ this._doneAt = new Error("place holder");
+
+ let requireBuiltIn = true;
+ try {
+ requireBuiltIn = Services.prefs.getBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ try {
+ CertUtils.checkCert(request.channel, !requireBuiltIn);
+ }
+ catch (e) {
+ logger.warn("Request failed: " + this.url + " - " + e);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ if (!Components.isSuccessCode(request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + request.status);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ let channel = request.channel;
+ if (channel instanceof Ci.nsIHttpChannel && !channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " + channel.responseStatus +
+ ": " + channel.responseStatusText);
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ return;
+ }
+
+ // Detect the manifest type by first attempting to parse it as
+ // JSON, and falling back to parsing it as XML if that fails.
+ let parser;
+ try {
+ try {
+ let json = JSON.parse(request.responseText);
+
+ parser = () => parseJSONManifest(this.id, this.updateKey, request, json);
+ } catch (e) {
+ if (!(e instanceof SyntaxError))
+ throw e;
+ let domParser = Cc["@mozilla.org/xmlextras/domparser;1"].createInstance(Ci.nsIDOMParser);
+ let xml = domParser.parseFromString(request.responseText, "text/xml");
+
+ if (xml.documentElement.namespaceURI == XMLURI_PARSE_ERROR)
+ throw new Error("Update manifest was not valid XML or JSON");
+
+ parser = () => parseRDFManifest(this.id, this.updateKey, request, xml);
+ }
+ } catch (e) {
+ logger.warn("onUpdateCheckComplete failed to determine manifest type");
+ this.notifyError(AddonUpdateChecker.ERROR_UNKNOWN_FORMAT);
+ return;
+ }
+
+ let results;
+ try {
+ results = parser();
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete failed to parse update manifest", e);
+ this.notifyError(AddonUpdateChecker.ERROR_PARSE_ERROR);
+ return;
+ }
+
+ if ("onUpdateCheckComplete" in this.observer) {
+ try {
+ this.observer.onUpdateCheckComplete(results);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckComplete notification failed", e);
+ }
+ }
+ else {
+ logger.warn("onUpdateCheckComplete may not properly cancel", new Error("stack marker"));
+ }
+ },
+
+ /**
+ * Called when the request times out
+ */
+ onTimeout: function() {
+ this.request = null;
+ this._doneAt = new Error("Timed out");
+ logger.warn("Request for " + this.url + " timed out");
+ this.notifyError(AddonUpdateChecker.ERROR_TIMEOUT);
+ },
+
+ /**
+ * Called when the manifest failed to load.
+ */
+ onError: function() {
+ if (!Components.isSuccessCode(this.request.status)) {
+ logger.warn("Request failed: " + this.url + " - " + this.request.status);
+ }
+ else if (this.request.channel instanceof Ci.nsIHttpChannel) {
+ try {
+ if (this.request.channel.requestSucceeded) {
+ logger.warn("Request failed: " + this.url + " - " +
+ this.request.channel.responseStatus + ": " +
+ this.request.channel.responseStatusText);
+ }
+ }
+ catch (e) {
+ logger.warn("HTTP Request failed for an unknown reason");
+ }
+ }
+ else {
+ logger.warn("Request failed for an unknown reason");
+ }
+
+ this.request = null;
+ this._doneAt = new Error("UP_onError");
+
+ this.notifyError(AddonUpdateChecker.ERROR_DOWNLOAD_ERROR);
+ },
+
+ /**
+ * Helper method to notify the observer that an error occured.
+ */
+ notifyError: function(aStatus) {
+ if ("onUpdateCheckError" in this.observer) {
+ try {
+ this.observer.onUpdateCheckError(aStatus);
+ }
+ catch (e) {
+ logger.warn("onUpdateCheckError notification failed", e);
+ }
+ }
+ },
+
+ /**
+ * Called to cancel an in-progress update check.
+ */
+ cancel: function() {
+ if (!this.request) {
+ logger.error("Trying to cancel already-complete request", this._doneAt);
+ return;
+ }
+ this.request.abort();
+ this.request = null;
+ this._doneAt = new Error("UP_cancel");
+ this.notifyError(AddonUpdateChecker.ERROR_CANCELLED);
+ }
+};
+
+/**
+ * Tests if an update matches a version of the application or platform
+ *
+ * @param aUpdate
+ * The available update
+ * @param aAppVersion
+ * The application version to use
+ * @param aPlatformVersion
+ * The platform version to use
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @param aCompatOverrides
+ * AddonCompatibilityOverride objects to match against. Optional.
+ * @return true if the update is compatible with the application/platform
+ */
+function matchesVersions(aUpdate, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (aCompatOverrides) {
+ let override = AddonRepository.findMatchingCompatOverride(aUpdate.version,
+ aCompatOverrides,
+ aAppVersion,
+ aPlatformVersion);
+ if (override && override.type == "incompatible")
+ return false;
+ }
+
+ if (aUpdate.strictCompatibility && !aIgnoreStrictCompat)
+ aIgnoreMaxVersion = false;
+
+ let result = false;
+ for (let app of aUpdate.targetApplications) {
+ if (app.id == Services.appinfo.ID) {
+ return (Services.vc.compare(aAppVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aAppVersion, app.maxVersion) <= 0));
+ }
+ if (app.id == TOOLKIT_ID) {
+ result = (Services.vc.compare(aPlatformVersion, app.minVersion) >= 0) &&
+ (aIgnoreMaxVersion || (Services.vc.compare(aPlatformVersion, app.maxVersion) <= 0));
+ }
+ }
+ return result;
+}
+
+this.AddonUpdateChecker = {
+ // These must be kept in sync with AddonManager
+ // The update check timed out
+ ERROR_TIMEOUT: -1,
+ // There was an error while downloading the update information.
+ ERROR_DOWNLOAD_ERROR: -2,
+ // The update information was malformed in some way.
+ ERROR_PARSE_ERROR: -3,
+ // The update information was not in any known format.
+ ERROR_UNKNOWN_FORMAT: -4,
+ // The update information was not correctly signed or there was an SSL error.
+ ERROR_SECURITY_ERROR: -5,
+ // The update was cancelled
+ ERROR_CANCELLED: -6,
+
+ /**
+ * Retrieves the best matching compatibility update for the application from
+ * a list of available update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aVersion
+ * The version of the add-on to get new compatibility information for
+ * @param aIgnoreCompatibility
+ * An optional parameter to get the first compatibility update that
+ * is compatible with any version of the application or toolkit
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * Ignore maxVersion when testing if an update matches. Optional.
+ * @param aIgnoreStrictCompat
+ * Ignore strictCompatibility when testing if an update matches. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getCompatibilityUpdate: function(aUpdates, aVersion, aIgnoreCompatibility,
+ aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ for (let update of aUpdates) {
+ if (Services.vc.compare(update.version, aVersion) == 0) {
+ if (aIgnoreCompatibility) {
+ for (let targetApp of update.targetApplications) {
+ let id = targetApp.id;
+ if (id == Services.appinfo.ID || id == TOOLKIT_ID)
+ return update;
+ }
+ }
+ else if (matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat)) {
+ return update;
+ }
+ }
+ }
+ return null;
+ },
+
+ /**
+ * Returns the newest available update from a list of update objects.
+ *
+ * @param aUpdates
+ * An array of update objects
+ * @param aAppVersion
+ * The version of the application or null to use the current version
+ * @param aPlatformVersion
+ * The version of the platform or null to use the current version
+ * @param aIgnoreMaxVersion
+ * When determining compatible updates, ignore maxVersion. Optional.
+ * @param aIgnoreStrictCompat
+ * When determining compatible updates, ignore strictCompatibility. Optional.
+ * @param aCompatOverrides
+ * Array of AddonCompatibilityOverride to take into account. Optional.
+ * @return an update object if one matches or null if not
+ */
+ getNewestCompatibleUpdate: function(aUpdates, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides) {
+ if (!aAppVersion)
+ aAppVersion = Services.appinfo.version;
+ if (!aPlatformVersion)
+ aPlatformVersion = Services.appinfo.platformVersion;
+
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+
+ let newest = null;
+ for (let update of aUpdates) {
+ if (!update.updateURL)
+ continue;
+ let state = blocklist.getAddonBlocklistState(update, aAppVersion, aPlatformVersion);
+ if (state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+ if ((newest == null || (Services.vc.compare(newest.version, update.version) < 0)) &&
+ matchesVersions(update, aAppVersion, aPlatformVersion,
+ aIgnoreMaxVersion, aIgnoreStrictCompat,
+ aCompatOverrides)) {
+ newest = update;
+ }
+ }
+ return newest;
+ },
+
+ /**
+ * Starts an update check.
+ *
+ * @param aId
+ * The ID of the add-on being checked for updates
+ * @param aUpdateKey
+ * An optional update key for the add-on
+ * @param aUrl
+ * The URL of the add-on's update manifest
+ * @param aObserver
+ * An observer to notify of results
+ * @return UpdateParser so that the caller can use UpdateParser.cancel() to shut
+ * down in-progress update requests
+ */
+ checkForUpdates: function(aId, aUpdateKey, aUrl, aObserver) {
+ return new UpdateParser(aId, aUpdateKey, aUrl, aObserver);
+ }
+};
diff --git a/toolkit/mozapps/extensions/internal/Content.js b/toolkit/mozapps/extensions/internal/Content.js
new file mode 100644
index 000000000..9f366ba32
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/Content.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* globals addMessageListener*/
+
+"use strict";
+
+(function() {
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+
+var {Services} = Cu.import("resource://gre/modules/Services.jsm", {});
+
+var nsIFile = Components.Constructor("@mozilla.org/file/local;1", "nsIFile",
+ "initWithPath");
+
+const MSG_JAR_FLUSH = "AddonJarFlush";
+const MSG_MESSAGE_MANAGER_CACHES_FLUSH = "AddonMessageManagerCachesFlush";
+
+
+try {
+ if (Services.appinfo.processType !== Services.appinfo.PROCESS_TYPE_DEFAULT) {
+ // Propagate JAR cache flush notifications across process boundaries.
+ addMessageListener(MSG_JAR_FLUSH, function(message) {
+ let file = new nsIFile(message.data);
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+ });
+ // Propagate message manager caches flush notifications across processes.
+ addMessageListener(MSG_MESSAGE_MANAGER_CACHES_FLUSH, function() {
+ Services.obs.notifyObservers(null, "message-manager-flush-caches", null);
+ });
+ }
+} catch (e) {
+ Cu.reportError(e);
+}
+
+})();
diff --git a/toolkit/mozapps/extensions/internal/E10SAddonsRollout.jsm b/toolkit/mozapps/extensions/internal/E10SAddonsRollout.jsm
new file mode 100644
index 000000000..3bcee44d3
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/E10SAddonsRollout.jsm
@@ -0,0 +1,982 @@
+/* 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 = [ "isAddonPartOfE10SRollout" ];
+
+const Cu = Components.utils;
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+
+const PREF_E10S_ADDON_BLOCKLIST = "extensions.e10s.rollout.blocklist";
+const PREF_E10S_ADDON_POLICY = "extensions.e10s.rollout.policy";
+
+const ADDONS = {
+ "Greasemonkey": { // Greasemonkey
+ id: "{e4a8a97b-f2ed-450b-b12d-ee082ba24781}", minVersion: "3.8",
+ },
+
+ "DYTV": { // Download YouTube Videos as MP4
+ id: "{b9bfaf1c-a63f-47cd-8b9a-29526ced9060}", minVersion: "1.8.7",
+ },
+
+ "VDH": { // Video Download Helper
+ id: "{b9db16a4-6edc-47ec-a1f4-b86292ed211d}", minVersion: "5.6.1",
+ },
+
+ "Lightbeam": { // Lightbeam
+ id: "jid1-F9UJ2thwoAm5gQ@jetpack", minVersion: "1.3.0.1",
+ },
+
+ "ABP": { // Adblock Plus
+ id: "{d10d0bf8-f5b5-c8b4-a8b2-2b9879e08c5d}", minVersion: "2.7.3",
+ },
+
+ "uBlockOrigin": { // uBlock Origin
+ id: "uBlock0@raymondhill.net", minVersion: "1.7.6",
+ },
+
+ "Emoji": { // Emoji Cheatsheet
+ id: "jid1-Xo5SuA6qc1DFpw@jetpack", minVersion: "1.1.1",
+ },
+
+ "ASP": { // Awesome Screenshot Plus
+ id: "jid0-GXjLLfbCoAx0LcltEdFrEkQdQPI@jetpack", minVersion: "3.0.10",
+ },
+
+ "PersonasPlus": { // PersonasPlus
+ id: "personas@christopher.beard", minVersion: "1.8.0",
+ },
+
+ "ACR": { // Add-on Compatibility Reporter
+ id: "compatibility@addons.mozilla.org", minVersion: "2.2.0",
+ },
+
+ // Add-ons used for testing
+ "test1": {
+ id: "bootstrap1@tests.mozilla.org", minVersion: "1.0",
+ },
+
+ "test2": {
+ id: "bootstrap2@tests.mozilla.org", minVersion: "1.0",
+ },
+};
+
+// NOTE: Do not modify sets or policies after they have already been
+// published to users. They must remain unchanged to provide valid data.
+
+// Set 2 used during 48 Beta cycle. Kept here for historical reasons.
+const set2 = [ADDONS.Greasemonkey,
+ ADDONS.DYTV,
+ ADDONS.VDH,
+ ADDONS.Lightbeam,
+ ADDONS.ABP,
+ ADDONS.uBlockOrigin,
+ ADDONS.Emoji,
+ ADDONS.ASP,
+ ADDONS.PersonasPlus];
+
+const set49Release = [
+ ADDONS.Greasemonkey,
+ ADDONS.DYTV,
+ ADDONS.VDH,
+ ADDONS.Lightbeam,
+ ADDONS.ABP,
+ ADDONS.uBlockOrigin,
+ ADDONS.Emoji,
+ ADDONS.ASP,
+ ADDONS.PersonasPlus,
+ ADDONS.ACR
+];
+
+// These are only the add-ons in the Add-Ons Manager Discovery
+// pane. This set is here in case we need to reduce add-ons
+// exposure live on Release.
+const set49PaneOnly = [
+ ADDONS.ABP,
+ ADDONS.VDH,
+ ADDONS.Emoji,
+ ADDONS.ASP,
+ ADDONS.ACR
+]
+
+// ================== ADDONS FOR 51 RELEASE ==================
+//
+// During the 51 beta cycle, we tested e10s with all addons
+// except those explicitly marked as being incompatible.
+// For release, instead of opening this up, we assembled
+// the lists below with all addons that were seen on beta
+// and had over 50 installs.
+//
+// This list is in a new format to allow fast access and also
+// to allow controlling by the number of addons installed.
+
+const set51Release = {
+ "_65Members_@download.fromdoctopdf.com": {minVersion: "7.102.10.4221", installs: 32092},
+ "light_plugin_ACF0E80077C511E59DED005056C00008@kaspersky.com": {minVersion: "4.6.3-15", installs: 27758},
+ "_ceMembers_@free.easypdfcombine.com": {minVersion: "7.102.10.4117", installs: 17797},
+ "caa1-aDOiCAxFFMOVIX@jetpack": {minVersion: "0.1.7", installs: 13150},
+ "{4ED1F68A-5463-4931-9384-8FFF5ED91D92}": {minVersion: "5.0.248.0", installs: 12774},
+ "_dbMembers_@free.getformsonline.com": {minVersion: "7.102.10.4251", installs: 11909},
+ "_4zMembers_@www.videodownloadconverter.com": {minVersion: "7.102.10.5033", installs: 11612},
+ "light_plugin_F6F079488B53499DB99380A7E11A93F6@kaspersky.com": {minVersion: "5.0.141-4-20161031140250", installs: 10944},
+ "YoutubeDownloader@PeterOlayev.com": {minVersion: "2.4.1", installs: 10722},
+ "{82AF8DCA-6DE9-405D-BD5E-43525BDAD38A}": {minVersion: "8.0.0.9103", installs: 8856},
+ "client@anonymox.net": {minVersion: "2.5.2", installs: 8225},
+ "_8hMembers_@download.allin1convert.com": {minVersion: "7.102.10.3584", installs: 7681},
+ "light_plugin_D772DC8D6FAF43A29B25C4EBAA5AD1DE@kaspersky.com": {minVersion: "4.6.2-42-20160922074409", installs: 7177},
+ "_dzMembers_@www.pconverter.com": {minVersion: "7.102.10.4851", installs: 7115},
+ "fxdevtools-adapters@mozilla.org": {minVersion: "0.3.5", installs: 6926},
+ "_9pMembers_@free.onlinemapfinder.com": {minVersion: "7.102.10.4836", installs: 6583},
+ "@DownloadManager": {minVersion: "0.2.1", installs: 6412},
+ "ar1er-ewrgfdgomusix@jetpack": {minVersion: "1.0.6", installs: 5975},
+ "_agMembers_@free.premierdownloadmanager.com": {minVersion: "7.102.10.4846", installs: 5605},
+ "_paMembers_@www.filmfanatic.com": {minVersion: "7.102.10.4163", installs: 5448},
+ "_gtMembers_@free.gamingwonderland.com": {minVersion: "7.102.10.4263", installs: 5241},
+ "LVD-SAE@iacsearchandmedia.com": {minVersion: "8.5", installs: 4694},
+ "_fsMembers_@free.pdfconverterhq.com": {minVersion: "7.102.10.4849", installs: 4526},
+ "_6xMembers_@www.readingfanatic.com": {minVersion: "7.102.10.4914", installs: 4417},
+ "@mysmartprice-ff": {minVersion: "0.0.6", installs: 4381},
+ "jid1-YcMV6ngYmQRA2w@jetpack": {minVersion: "1.37.9", installs: 3899},
+ "{58d735b4-9d6c-4e37-b146-7b9f7e79e318}": {minVersion: "1.6", installs: 3733},
+ "anttoolbar@ant.com": {minVersion: "2.4.7.47", installs: 3720},
+ "adblockpopups@jessehakanen.net": {minVersion: "0.9.2.1-signed.1-signed", installs: 3602},
+ "ERAIL.IN.FFPLUGIN@jetpack": {minVersion: "6.0.rev142", installs: 3545},
+ "WebProtection@360safe.com": {minVersion: "5.0.0.1005", installs: 3475},
+ "yasearch@yandex.ru": {minVersion: "8.20.4", installs: 3299},
+ "{19503e42-ca3c-4c27-b1e2-9cdb2170ee34}": {minVersion: "1.5.6.14", installs: 3106},
+ "{C1A2A613-35F1-4FCF-B27F-2840527B6556}": {minVersion: "2016.8.1.9", installs: 3083},
+ "_b7Members_@free.mytransitguide.com": {minVersion: "7.102.10.4812", installs: 3011},
+ "_9tMembers_@free.internetspeedtracker.com": {minVersion: "7.102.10.4339", installs: 2828},
+ "_64Members_@www.televisionfanatic.com": {minVersion: "7.102.10.4968", installs: 2821},
+ "info@youtube-mp3.org": {minVersion: "1.0.9.1-signed.1-signed", installs: 2717},
+ "ffext_basicvideoext@startpage24": {minVersion: "1.97.37.1-signed.1-signed", installs: 2663},
+ "MUB-SAE@iacsearchandmedia.com": {minVersion: "8.7", installs: 2650},
+ "_4jMembers_@www.radiorage.com": {minVersion: "7.102.10.4916", installs: 2631},
+ "@Email": {minVersion: "4.0.12", installs: 2583},
+ "_gcMembers_@www.weatherblink.com": {minVersion: "7.38.8.56523", installs: 2519},
+ "_dqMembers_@www.downspeedtest.com": {minVersion: "7.102.10.3827", installs: 2445},
+ "translator@zoli.bod": {minVersion: "2.1.0.5.1.1-signed", installs: 2310},
+ "{a38384b3-2d1d-4f36-bc22-0f7ae402bcd7}": {minVersion: "1.0.0.51", installs: 2190},
+ "_1eMembers_@www.videoscavenger.com": {minVersion: "7.38.8.45273", installs: 2185},
+ "tvplusnewtab-the-extension1@mozilla.com": {minVersion: "0.1.5", installs: 2155},
+ "homepage@mail.ru": {minVersion: "1.0.2", installs: 2124},
+ "search@mail.ru": {minVersion: "1.0.7", installs: 2038},
+ "_69Members_@www.packagetracer.com": {minVersion: "7.102.10.4831", installs: 2036},
+ "{7b8a500a-a464-4624-bd4f-73eaafe0f766}": {minVersion: "3", installs: 2027},
+ "paulsaintuzb@gmail.com": {minVersion: "8.2.1", installs: 2005},
+ "k7srff_enUS@k7computing.com": {minVersion: "2.4", installs: 1929},
+ "_e5Members_@www.productivityboss.com": {minVersion: "7.38.8.46590", installs: 1892},
+ "vdpure@link64": {minVersion: "1.97.43", installs: 1860},
+ "_9tMembers_@download.internetspeedtracker.com": {minVersion: "7.38.8.56171", installs: 1824},
+ "_g3Members_@free.easyphotoedit.com": {minVersion: "7.102.10.4108", installs: 1822},
+ "_64Members_@download.televisionfanatic.com": {minVersion: "7.38.9.3004", installs: 1730},
+ "_8iMembers_@download.audiotoaudio.com": {minVersion: "7.102.10.3585", installs: 1704},
+ "adblockultimate@adblockultimate.net": {minVersion: "2.25", installs: 1648},
+ "eagleget_ffext@eagleget.com": {minVersion: "3.8", installs: 1640},
+ "_9eMembers_@free.findmefreebies.com": {minVersion: "7.102.10.4193", installs: 1638},
+ "content_blocker_663BE8@kaspersky.com": {minVersion: "4.5.4.19.1", installs: 1625},
+ "virtual_keyboard_074028@kaspersky.com": {minVersion: "4.5.4.19.1", installs: 1624},
+ "browsec@browsec.com": {minVersion: "2.0.3", installs: 1610},
+ "@Maps": {minVersion: "4.0.0", installs: 1587},
+ "_exMembers_@free.easydocmerge.com": {minVersion: "7.102.10.4137", installs: 1493},
+ "{635abd67-4fe9-1b23-4f01-e679fa7484c1}": {minVersion: "5.0.2", installs: 1490},
+ "abb@amazon.com": {minVersion: "10.1612.1.304", installs: 1463},
+ "{1BC9BA34-1EED-42ca-A505-6D2F1A935BBB}": {minVersion: "6.2.18.1", installs: 1436},
+ "mp4downloader@jeff.net": {minVersion: "1.3.3.1-signed.1-signed", installs: 1410},
+ "jid1-16aeif9OQIRKxA@jetpack": {minVersion: "1.1.4", installs: 1399},
+ "{c45c406e-ab73-11d8-be73-000a95be3b12}": {minVersion: "1.2.11", installs: 1367},
+ "online_banking_08806E@kaspersky.com": {minVersion: "4.5.4.19.1", installs: 1356},
+ "_ewMembers_@free.mergedocsonline.com": {minVersion: "7.102.10.4710", installs: 1337},
+ "@DiscreteSearch": {minVersion: "0.2.1", installs: 1306},
+ "{6AC85730-7D0F-4de0-B3FA-21142DD85326}": {minVersion: "2.8.2", installs: 1286},
+ "{063DA41A-2561-401B-91FA-AC75E460F4EB}": {minVersion: "1.0.7.1", installs: 1280},
+ "netvideohunter@netvideohunter.com": {minVersion: "1.2", installs: 1260},
+ "_8eMembers_@download.howtosimplified.com": {minVersion: "7.102.10.4285", installs: 1230},
+ "FGZ-SAE@iacsearchandmedia.com": {minVersion: "8.5", installs: 1220},
+ "adguardadblocker@adguard.com": {minVersion: "2.4.14", installs: 1172},
+ "_39Members_@www.mapsgalaxy.com": {minVersion: "7.102.10.4730", installs: 1171},
+ "_euMembers_@free.filesendsuite.com": {minVersion: "7.102.10.4154", installs: 1166},
+ "_brMembers_@free.yourtemplatefinder.com": {minVersion: "7.102.10.5047", installs: 1159},
+ "_8jMembers_@download.myimageconverter.com": {minVersion: "7.102.10.4778", installs: 1150},
+ "_12Members_@free.myscrapnook.com": {minVersion: "7.102.10.4739", installs: 1113},
+ "_7eMembers_@www.homeworksimplified.com": {minVersion: "7.102.10.4290", installs: 1109},
+ "{fe272bd1-5f76-4ea4-8501-a05d35d823fc}": {minVersion: "2.1.9.1-signed.1-let-fixed.1-signed", installs: 1108},
+ "_frMembers_@free.testforspeed.com": {minVersion: "7.102.10.4993", installs: 1107},
+ "{068e178c-61a9-4a63-b74f-87404a6f5ea1}": {minVersion: "2", installs: 1104},
+ "@Package": {minVersion: "0.2.0", installs: 1092},
+ "6asa42dfa4784fsf368g@youtubeconverter.me": {minVersion: "0.1", installs: 1071},
+ "_diMembers_@www.free.easymaillogin.com": {minVersion: "7.102.10.4112", installs: 1043},
+ "_v4Members_@www.dictionaryboss.com": {minVersion: "7.102.10.3797", installs: 1035},
+ "colorPicker@colorPicker": {minVersion: "3.0.1-signed.1-signed", installs: 1023},
+ "hotspot-shield@anchorfree.com": {minVersion: "1.2.87", installs: 1000},
+ "manishjain9@hotmail.com_easiestyoutube": {minVersion: "7.2.1-signed.1-let-fixed.1-signed", installs: 993},
+ "{cd617375-6743-4ee8-bac4-fbf10f35729e}": {minVersion: "2.9.6", installs: 987},
+ "@Converter": {minVersion: "4.1.0", installs: 986},
+ "{dd3d7613-0246-469d-bc65-2a3cc1668adc}": {minVersion: "1.1.8.1-signed.1-signed", installs: 983},
+ "ubufox@ubuntu.com": {minVersion: "3.2", installs: 950},
+ "jid1-lpoiffmusixlib@jetpack": {minVersion: "0.1.9", installs: 945},
+ "_5aMembers_@download.mywebface.com": {minVersion: "7.102.10.4837", installs: 930},
+ "leethax@leethax.net": {minVersion: "2016.12.02", installs: 930},
+ "{1A2D0EC4-75F5-4c91-89C4-3656F6E44B68}": {minVersion: "0.6.3.1-signed.1-signed", installs: 885},
+ "{64161300-e22b-11db-8314-0800200c9a66}": {minVersion: "0.9.6.18", installs: 875},
+ "_bfMembers_@free.snapmyscreen.com": {minVersion: "7.102.10.4951", installs: 827},
+ "uriloader@pdf.js": {minVersion: "1.0.277.1-signed.1-signed", installs: 815},
+ "{e968fc70-8f95-4ab9-9e79-304de2a71ee1}": {minVersion: "0.7.3.1-signed.1-signed", installs: 805},
+ "save-as-pdf-ff@pdfcrowd.com": {minVersion: "1.5.1-signed.1-signed", installs: 804},
+ "{75CEEE46-9B64-46f8-94BF-54012DE155F0}": {minVersion: "0.4.15", installs: 794},
+ "safesearchplus2@avira.com": {minVersion: "1.4.1.371", installs: 786},
+ "easyscreenshot@mozillaonline.com": {minVersion: "1.2.8", installs: 785},
+ "_eeMembers_@download.freeradiocast.com": {minVersion: "7.38.8.46366", installs: 783},
+ "_89Members_@download.safepcrepair.com": {minVersion: "7.39.8.51080", installs: 777},
+ "{a3a5c777-f583-4fef-9380-ab4add1bc2a5}": {minVersion: "2.4.2.1-signed", installs: 771},
+ "content_blocker@kaspersky.com": {minVersion: "4.0.10.15", installs: 770},
+ "safesearch@avira.com": {minVersion: "1.4.1.371", installs: 767},
+ "youtube2mp3@mondayx.de": {minVersion: "1.2.3.1-signed.1-signed", installs: 748},
+ "2020Player_IKEA@2020Technologies.com": {minVersion: "5.0.94.1", installs: 736},
+ "_edMembers_@free.myradioaccess.com": {minVersion: "7.102.10.4797", installs: 734},
+ "_dmMembers_@free.gounzip.com": {minVersion: "7.102.10.4277", installs: 733},
+ "Media-Newtab-the-extension1@mozilla.com": {minVersion: "0.1.6", installs: 732},
+ "foxmarks@kei.com": {minVersion: "4.3.19", installs: 728},
+ "{e8deb9e5-5688-4655-838a-b7a121a9f16e}": {minVersion: "48.4", installs: 726},
+ "{195A3098-0BD5-4e90-AE22-BA1C540AFD1E}": {minVersion: "4.1.0.1-signed.1-signed", installs: 722},
+ "jid1-4P0kohSJxU1qGg@jetpack": {minVersion: "1.22.550", installs: 719},
+ "DailymotionVideoDownloader@PeterOlayev.com": {minVersion: "1.0.6.1-signed.1-signed", installs: 717},
+ "jid1-P34HaABBBpOerQ@jetpack": {minVersion: "0.2.1-signed.1-signed", installs: 715},
+ "SQLiteManager@mrinalkant.blogspot.com": {minVersion: "0.8.3.1-signed.1-signed", installs: 700},
+ "2.0@disconnect.me": {minVersion: "3.15.3.1-signed.1-signed", installs: 693},
+ "multifox@hultmann": {minVersion: "3.2.3", installs: 690},
+ "_5mMembers_@download.myfuncards.com": {minVersion: "7.102.10.4783", installs: 679},
+ "_btMembers_@free.catsandcatapults.com": {minVersion: "7.102.10.3677", installs: 673},
+ "pavel.sherbakov@gmail.com": {minVersion: "19.1.1", installs: 666},
+ "_fbMembers_@free.smarterpassword.com": {minVersion: "7.102.10.4936", installs: 644},
+ "jid2-l8SPBzHJWBIiHQ@jetpack": {minVersion: "3.1", installs: 639},
+ "{B17C1C5A-04B1-11DB-9804-B622A1EF5492}": {minVersion: "1.3.2", installs: 633},
+ "myplaycitycom@gametab": {minVersion: "1.6", installs: 616},
+ "{ad0d925d-88f8-47f1-85ea-8463569e756e}": {minVersion: "2.0.5", installs: 604},
+ "{37964A3C-4EE8-47b1-8321-34DE2C39BA4D}": {minVersion: "2.5.4.174", installs: 603},
+ "youtubemp3podcaster@jeremy.d.gregorio.com": {minVersion: "3.9.0", installs: 601},
+ "caa1-aDOiCAxFFPRIVATE@jetpack": {minVersion: "0.2.0", installs: 598},
+ "_f5Members_@free.typingfanatic.com": {minVersion: "7.102.10.5014", installs: 595},
+ "_94Members_@www.motitags.com": {minVersion: "7.102.10.4744", installs: 594},
+ "{888d99e7-e8b5-46a3-851e-1ec45da1e644}": {minVersion: "45.0.0", installs: 581},
+ "_1cMembers_@www.bringmesports.com": {minVersion: "7.102.10.3646", installs: 580},
+ "{a6fd85ed-e919-4a43-a5af-8da18bda539f}": {minVersion: "2.9.1.1-signed", installs: 572},
+ "{0fc22c4c-93ed-48ea-ad12-dc8039cf3795}": {minVersion: "1.3", installs: 568},
+ "homeutil@yandex.ru": {minVersion: "1.0.13", installs: 565},
+ "_doMembers_@free.convertanyfile.com": {minVersion: "7.38.8.45860", installs: 563},
+ "SocialNewPages-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 561},
+ "wappalyzer@crunchlabz.com": {minVersion: "3.2.7", installs: 557},
+ "_5qMembers_@www.zwinky.com": {minVersion: "7.38.8.45270", installs: 551},
+ "{0545b830-f0aa-4d7e-8820-50a4629a56fe}": {minVersion: "31.0.9", installs: 531},
+ "vk@sergeykolosov.mp": {minVersion: "0.3.9.5", installs: 522},
+ "{77b819fa-95ad-4f2c-ac7c-486b356188a9}": {minVersion: "4.0.20130422.1-signed.1-signed", installs: 505},
+ "@true-key": {minVersion: "1.23.0.2433", installs: 501},
+ "_1pMembers_@www.referenceboss.com": {minVersion: "7.102.10.4932", installs: 499},
+ "{C7AE725D-FA5C-4027-BB4C-787EF9F8248A}": {minVersion: "1.0.0.4", installs: 494},
+ "alx-ffdeveloper@amazon.com": {minVersion: "3.0.2", installs: 493},
+ "{3d7eb24f-2740-49df-8937-200b1cc08f8a}": {minVersion: "1.5.20", installs: 491},
+ "_1gMembers_@www.inboxace.com": {minVersion: "7.38.8.56535", installs: 488},
+ "{7DD78D43-0962-4d9b-BC76-ABF13B3B2ED1}": {minVersion: "3.5.0.1428", installs: 484},
+ "imageblock@hemantvats.com": {minVersion: "3.1", installs: 472},
+ "online_banking@kaspersky.com": {minVersion: "4.0.10.15", installs: 463},
+ "virtual_keyboard@kaspersky.com": {minVersion: "4.0.10.15", installs: 463},
+ "button@scholar.google.com": {minVersion: "1.1.1-signed.1-signed", installs: 463},
+ "anti_banner@kaspersky.com": {minVersion: "4.0.10.15", installs: 462},
+ "url_advisor@kaspersky.com": {minVersion: "4.0.10.15", installs: 461},
+ "{6d96bb5e-1175-4ebf-8ab5-5f56f1c79f65}": {minVersion: "0.9.8", installs: 457},
+ "_14Members_@download.totalrecipesearch.com": {minVersion: "7.102.10.4983", installs: 456},
+ "{394DCBA4-1F92-4f8e-8EC9-8D2CB90CB69B}": {minVersion: "5.1.1", installs: 447},
+ "_57Members_@free.marineaquariumfree.com": {minVersion: "7.102.10.4716", installs: 446},
+ "e67f8350-7edf-11e3-baa7-0800200c9a66@fri-gate.org": {minVersion: "2.2.1.1-signed", installs: 446},
+ "FireXPath@pierre.tholence.com": {minVersion: "0.9.7.1.1-signed.1-signed", installs: 442},
+ "@youtube_downloader": {minVersion: "0.0.9", installs: 435},
+ "ff_hpset@jetpack": {minVersion: "1.0.8", installs: 428},
+ "{d0bfdcce-52c7-4b32-bb45-948f62db8d3f}": {minVersion: "49.1", installs: 406},
+ "_j2Members_@www.soccerinferno.com": {minVersion: "7.102.10.4948", installs: 405},
+ "autoform@olifozzy": {minVersion: "1.2.4.1-signed.1-signed", installs: 405},
+ "FunSafeTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 405},
+ "testpilot@labs.mozilla.com": {minVersion: "1.2.3.1-signed", installs: 405},
+ "vwof@drev.com": {minVersion: "3.1.2", installs: 401},
+ "_ftMembers_@free.mytelevisionhq.com": {minVersion: "7.102.10.4817", installs: 397},
+ "{e001c731-5e37-4538-a5cb-8168736a2360}": {minVersion: "0.9.9.152", installs: 396},
+ "{95E84BD3-3604-4AAC-B2CA-D9AC3E55B64B}": {minVersion: "2.0.0.78", installs: 393},
+ "_8lMembers_@free.filesharefanatic.com": {minVersion: "7.102.10.4171", installs: 389},
+ "clipconverter@clipconverter.cc": {minVersion: "1.5.2", installs: 387},
+ "_7jMembers_@download.gardeningenthusiast.com": {minVersion: "7.102.10.4260", installs: 383},
+ "antmark@ant.com": {minVersion: "1.1.14", installs: 383},
+ "_flMembers_@free.myformsfinder.com": {minVersion: "7.102.10.4784", installs: 381},
+ "{c36177c0-224a-11da-8cd6-0800200c9a91}": {minVersion: "3.9.85.1-signed.1-signed", installs: 375},
+ "@searchincognito": {minVersion: "0.1.0", installs: 375},
+ "{f13b157f-b174-47e7-a34d-4815ddfdfeb8}": {minVersion: "0.9.89.1-signed.1-signed", installs: 373},
+ "_5eMembers_@www.translationbuddy.com": {minVersion: "7.38.8.45962", installs: 372},
+ "{9c51bd27-6ed8-4000-a2bf-36cb95c0c947}": {minVersion: "11.0.1.1-signed.1-signed", installs: 370},
+ "clickclean@hotcleaner.com": {minVersion: "4.1.1-signed.1-signed", installs: 366},
+ "jid1-xKH0EoS44u1a2w@jetpack": {minVersion: "0.1.1-signed.1-signed", installs: 366},
+ "{c2056674-a37f-4b29-9300-2004759d74fe}": {minVersion: "2.0.0.1090", installs: 361},
+ "newtab-tv-the-extension1@mozilla.com": {minVersion: "0.1.5", installs: 359},
+ "ascsurfingprotectionnew@iobit.com": {minVersion: "2.1.3", installs: 355},
+ "FunTabSafe-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 353},
+ "d.lehr@chello.at": {minVersion: "1.2", installs: 350},
+ "anticontainer@downthemall.net": {minVersion: "1.5", installs: 348},
+ "{F8A55C97-3DB6-4961-A81D-0DE0080E53CB}": {minVersion: "1.0.10", installs: 347},
+ "@FormsApp": {minVersion: "0.2.0", installs: 346},
+ "multilinksplus@hugsmile.eu": {minVersion: "3.9.3", installs: 343},
+ "jid1-KWFaW5zc0EbtBQ@jetpack": {minVersion: "0.2.0", installs: 335},
+ "{e8f509f0-b677-11de-8a39-0800200c9a66}": {minVersion: "1.12.1-signed.1-let-fixed.1-signed", installs: 334},
+ "{37E4D8EA-8BDA-4831-8EA1-89053939A250}": {minVersion: "3.0.0.2.1-signed.1-signed", installs: 333},
+ "{c8d3bc80-0810-4d21-a2c2-be5f2b2832ac}": {minVersion: "0.98", installs: 332},
+ "{cb40da56-497a-4add-955d-3377cae4c33b}": {minVersion: "10.2.0.271", installs: 331},
+ "{5546F97E-11A5-46b0-9082-32AD74AAA920}": {minVersion: "0.76.1-signed.1-signed", installs: 325},
+ "_14Members_@www.totalrecipesearch.com": {minVersion: "7.38.8.45925", installs: 324},
+ "info@mp3it.eu": {minVersion: "1.4.1.1-signed.1-signed", installs: 324},
+ "firefox-autofill@googlegroups.com": {minVersion: "3.6.1-signed.1-signed", installs: 317},
+ "jid1-TQvJxTBYHA8qXg@jetpack": {minVersion: "0.4.1-signed.1-signed", installs: 315},
+ "{8f8fe09b-0bd3-4470-bc1b-8cad42b8203a}": {minVersion: "0.17.1-signed.1-signed", installs: 311},
+ "{D4DD63FA-01E4-46a7-B6B1-EDAB7D6AD389}": {minVersion: "0.9.10.1-signed.1-signed", installs: 311},
+ "{d7f46ca0-899d-11da-a72b-0800200c9a65}": {minVersion: "0.1.2.1-signed.1-signed", installs: 311},
+ "twoo@twoo.com": {minVersion: "1.6.0.1-signed", installs: 303},
+ "_29Members_@www.headlinealley.com": {minVersion: "7.38.8.56537", installs: 302},
+ "_e2Members_@free.coolpopulargames.com": {minVersion: "7.38.8.45873", installs: 300},
+ "TopTVTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 300},
+ "tmbepff@trendmicro.com": {minVersion: "9.2.0.1026", installs: 293},
+ "_2vMembers_@www.dailybibleguide.com": {minVersion: "7.38.8.52880", installs: 289},
+ "{54e46280-0211-11e3-b778-0800200c9a66}": {minVersion: "0.3", installs: 285},
+ "_49Members_@www.utilitychest.com": {minVersion: "7.38.8.45977", installs: 284},
+ "amcontextmenu@loucypher": {minVersion: "0.4.2.1-signed.1-signed", installs: 284},
+ "jid1-r1tDuNiNb4SEww@jetpack": {minVersion: "1.1.2673", installs: 283},
+ "_erMembers_@free.getvideoconvert.com": {minVersion: "7.102.10.5038", installs: 281},
+ "{b1df372d-8b32-4c7d-b6b4-9c5b78cf6fb1}": {minVersion: "0.87.1-signed.1-signed", installs: 281},
+ "jid1-cHKBMlArKdIVEg@jetpack": {minVersion: "1.24.1-signed.1-signed", installs: 281},
+ "@90B817C8-8A5C-413B-9DDD-B2C61ED6E79A": {minVersion: "1.09", installs: 278},
+ "smarterwiki@wikiatic.com": {minVersion: "5.2.1.1-signed.1-signed", installs: 278},
+ "whatsapppanel@alejandrobrizuela.com.ar": {minVersion: "1.1.1.1-signed.1-signed", installs: 277},
+ "lazarus@interclue.com": {minVersion: "2.3.1-signed.1-signed", installs: 275},
+ "{DEDA1132-B316-11DD-8BC1-4E5D56D89593}": {minVersion: "0.18", installs: 274},
+ "_h2Members_@free.calendarspark.com": {minVersion: "7.102.10.3641", installs: 273},
+ "@youtubedownloadere": {minVersion: "0.0.1", installs: 273},
+ "multirevenue@googlemail.com": {minVersion: "6.1.1", installs: 272},
+ "_d9Members_@www.everydaylookup.com": {minVersion: "7.102.10.4140", installs: 271},
+ "belgiumeid@eid.belgium.be": {minVersion: "1.0.21", installs: 271},
+ "{62DD0A97-FDD4-421b-94A5-D1A9434450C7}": {minVersion: "3.1", installs: 270},
+ "the-addon-bar@GeekInTraining-GiT": {minVersion: "3.2.9-compat-fixed-4", installs: 264},
+ "@phextension": {minVersion: "6.0.2", installs: 262},
+ "FunMediaTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 262},
+ "{7f57cf46-4467-4c2d-adfa-0cba7c507e54}": {minVersion: "4.0.1", installs: 259},
+ "safefacebook@bkav": {minVersion: "1.0.4", installs: 255},
+ "content_blocker_6418E0D362104DADA084DC312DFA8ABC@kaspersky.com": {minVersion: "4.5.3.8", installs: 254},
+ "virtual_keyboard_294FF26A1D5B455495946778FDE7CEDB@kaspersky.com": {minVersion: "4.5.3.8", installs: 254},
+ "{B821BF60-5C2D-41EB-92DC-3E4CCD3A22E4}": {minVersion: "4.3.1.10", installs: 252},
+ "@E9438230-A7DF-4D1F-8F2D-CA1D0F0F7924": {minVersion: "1.08.8.66", installs: 252},
+ "jid1-6MGm94JnyY2VkA@jetpack": {minVersion: "2.1.8", installs: 250},
+ "{20a82645-c095-46ed-80e3-08825760534b}": {minVersion: "1.3.1.1-signed", installs: 246},
+ "{a192bf54-089f-4325-ac25-7eafcd17a342}": {minVersion: "3.2", installs: 246},
+ "e389d8c2-5554-4ba2-a36e-ac7a57093130@gmail.com": {minVersion: "1.44.275", installs: 244},
+ "yslow@yahoo-inc.com": {minVersion: "3.1.8.1-signed.1-signed", installs: 244},
+ "avg@safeguard": {minVersion: "19.6.0.592", installs: 243},
+ "@windscribeff": {minVersion: "0.1.43", installs: 242},
+ "jid1-PBNne26X1Kn6hQ@jetpack": {minVersion: "3.3.3", installs: 240},
+ "{53A03D43-5363-4669-8190-99061B2DEBA5}": {minVersion: "1.5.14", installs: 239},
+ "@offersolymp": {minVersion: "0.0.2", installs: 238},
+ "firefox@dotvpn.com": {minVersion: "1.0.2", installs: 238},
+ "{62760FD6-B943-48C9-AB09-F99C6FE96088}": {minVersion: "4.2.9", installs: 236},
+ "jid1-sNL73VCI4UB0Fw@jetpack": {minVersion: "2.1.4", installs: 236},
+ "low_quality_flash@pie2k.com": {minVersion: "0.2.1-signed.1-signed", installs: 236},
+ "jid1-l6VQSR2FeKnliQ@jetpack": {minVersion: "2.0.1-signed", installs: 235},
+ "_5zMembers_@www.couponxplorer.com": {minVersion: "7.102.10.3738", installs: 234},
+ "adonis.cuhk@gmail.com": {minVersion: "1.8.9.1-signed.1-signed", installs: 234},
+ "_e1Members_@free.actionclassicgames.com": {minVersion: "7.38.8.45834", installs: 232},
+ "{ea4637dc-e014-4c17-9c2c-879322d23268}": {minVersion: "2.1.1-signed.1-signed", installs: 229},
+ "{4DC70064-89E2-4a55-8FC6-E8CDEAE3618C}": {minVersion: "0.7.7.1-signed.1-signed", installs: 228},
+ "odyssey_crypto_control@odysseytec.com": {minVersion: "3.5", installs: 228},
+ "seostatus@rubyweb": {minVersion: "1.5.9.1-signed.1-signed", installs: 228},
+ "_apMembers_@free.puzzlegamesdaily.com": {minVersion: "7.102.10.4865", installs: 227},
+ "@safesearchincognito": {minVersion: "0.1.8", installs: 226},
+ "jid1-HfCj61J5q2gaGQ@jetpack": {minVersion: "1.0.3", installs: 224},
+ "@stopads": {minVersion: "0.0.4", installs: 224},
+ "dta3noaero@vano": {minVersion: "1.0.1", installs: 224},
+ "rainbow@colors.org": {minVersion: "1.6.1-signed.1-signed", installs: 223},
+ "{146f1820-2b0d-49ef-acbf-d85a6986e10c}": {minVersion: "0.1.9.3.1-signed.1-signed", installs: 222},
+ "{b2bfe60c-eef8-4e20-8334-c53afdc1ffdd}": {minVersion: "3.2", installs: 222},
+ "{b7870b41-bfb3-44cd-8cc2-e392e51b0874}": {minVersion: "3.8", installs: 222},
+ "printPages2Pdf@reinhold.ripper": {minVersion: "0.1.9.3.1-signed", installs: 221},
+ "YouTubetoALL@ALLPlayer.org": {minVersion: "0.8.5.1-signed.1-signed", installs: 221},
+ "{7a526449-3a92-426f-8ca4-47439918f2b1}": {minVersion: "3.2", installs: 219},
+ "jdwimqhayu@yahoo.com": {minVersion: "0.0.0.6", installs: 219},
+ "{54FBE89E-C878-46bb-A064-AB327EE26EBC}": {minVersion: "3.8", installs: 214},
+ "modernDownloadManager@teo.pl": {minVersion: "0.2.2", installs: 214},
+ "{eb8fff7e-1dce-4f3f-a51d-d9513ed6bab4}": {minVersion: "3.8", installs: 211},
+ "jid0-YQz0l1jthOIz179ehuitYAOdBEs@jetpack": {minVersion: "2.0.2", installs: 211},
+ "{7e80e173-7e63-464e-8252-fe170b15c15a}": {minVersion: "2.3", installs: 210},
+ "{35d6291e-1d4b-f9b4-c52f-77e6410d1326}": {minVersion: "4.11.1.0", installs: 209},
+ "{3c59c791-aeec-44bb-af60-ff112eea18e3}": {minVersion: "3.2", installs: 209},
+ "{90477448-b59c-48cd-98af-6a298cbc15d2}": {minVersion: "3.8", installs: 209},
+ "{24d26487-6274-48b1-b500-22f24884f971}": {minVersion: "2.3", installs: 208},
+ "{b7389dbc-6646-412f-bbd5-53168ee68a98}": {minVersion: "49", installs: 208},
+ "{22181a4d-af90-4ca3-a569-faed9118d6bc}": {minVersion: "11.0.0.1181", installs: 207},
+ "printpdf@pavlov.net": {minVersion: "0.76.1-signed.1-signed", installs: 207},
+ "@com.virtualjame.disableads": {minVersion: "0.1.0", installs: 206},
+ "{9AA46F4F-4DC7-4c06-97AF-6665170634FE}": {minVersion: "1.11.6.1-signed.1-signed", installs: 205},
+ "tinyjsdebugger@enigmail.net": {minVersion: "1.1.5", installs: 204},
+ "_foMembers_@free.flightsearchapp.com": {minVersion: "7.102.10.4176", installs: 202},
+ "jid1-rs90nxQtPi3Asg@jetpack": {minVersion: "1.8.1-signed.1-signed", installs: 201},
+ "vlcplaylist@helgatauscher.de": {minVersion: "0.8.1-signed.1-signed", installs: 201},
+ "jid1-G80Ec8LLEbK5fQ@jetpack": {minVersion: "1.3.8", installs: 200},
+ "_gpMembers_@free.mymapswizard.com": {minVersion: "7.102.10.4775", installs: 199},
+ "BestMediaTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 199},
+ "info@convert2mp3.net": {minVersion: "2.5.1-signed.1-signed", installs: 199},
+ "partnerdefaults@mozilla.com": {minVersion: "1.0.1", installs: 199},
+ "qwantcomforfirefox@jetpack": {minVersion: "3.0.28", installs: 199},
+ "{65e41d20-f092-41b7-bb83-c6e8a9ab0f57}": {minVersion: "1.2.6", installs: 198},
+ "amznUWL2@amazon.com": {minVersion: "1.11", installs: 197},
+ "{1b80ae74-4912-44fc-9f27-30f9252a5ad7}": {minVersion: "2.3", installs: 197},
+ "{c9b4cd26-6f0e-4972-a9e0-8b77e811aa8f}": {minVersion: "2.3", installs: 197},
+ "shopcbtoolbar2@befrugal.com": {minVersion: "2013.3.23.1", installs: 197},
+ "trafficlight@bitdefender.com": {minVersion: "0.2.23.1-signed.1-signed", installs: 197},
+ "webrank-toolbar@probcomp.com": {minVersion: "4.4.1.1-signed.1-signed", installs: 197},
+ "_4lMembers_@www.bibletriviatime.com": {minVersion: "7.102.10.4330", installs: 196},
+ "xthunder@lshai.com": {minVersion: "1.3.4.1-signed.1-signed", installs: 196},
+ "extension@hidemyass.com": {minVersion: "1.3.2", installs: 195},
+ "jid1-MIAJd5BiK7V4Pw@jetpack": {minVersion: "0.9.1-signed.1-signed", installs: 195},
+ "{51aa69f8-8825-4def-916a-a766c5e3c0fd}": {minVersion: "3.8", installs: 194},
+ "{2bc72c53-9bde-4db2-8479-eda9a5e71f4e}": {minVersion: "3.2", installs: 193},
+ "{a95d8332-e4b4-6e7f-98ac-20b733364387}": {minVersion: "1.0.5", installs: 191},
+ "ocr@babylon.com": {minVersion: "1.1", installs: 191},
+ "{d3b9472c-f8b1-4a10-935b-1087bac8417f}": {minVersion: "3.8", installs: 189},
+ "windowpromo@dm73.net": {minVersion: "1.6", installs: 188},
+ "alldownloader@link64": {minVersion: "1.00.17.1-signed.1-signed", installs: 187},
+ "{3e0e7d2a-070f-4a47-b019-91fe5385ba79}": {minVersion: "3.6.5.2", installs: 185},
+ "jid1-vFmnfCkyf5VeSA@jetpack": {minVersion: "0.4.0", installs: 185},
+ "@greatdealz": {minVersion: "0.0.3", installs: 184},
+ "superstart@enjoyfreeware.org": {minVersion: "7.4.0.1-signed", installs: 183},
+ "{c2fc3c2b-a65a-453c-bf95-101fde56ed1d}": {minVersion: "2.3", installs: 182},
+ "{53152e75-fd90-472f-9d30-5cba3679eab9}": {minVersion: "48.3", installs: 180},
+ "jid0-raWjElI57dRa4jx9CCiYm5qZUQU@jetpack": {minVersion: "3.0.12.1.1-signed.1-signed", installs: 180},
+ "_ivMembers_@free.simplepictureedit.com": {minVersion: "7.102.10.14166", installs: 179},
+ "jid1-wKRSK9TpFpr9Hw@jetpack": {minVersion: "0.92", installs: 179},
+ "emailExtractor@penzil.com": {minVersion: "1.3.1-signed.1-signed", installs: 178},
+ "{60B7679C-BED9-11E5-998D-8526BB8E7F8B}": {minVersion: "6.3", installs: 177},
+ "@pdfit": {minVersion: "0.1.9", installs: 177},
+ "jid1-6AyZ1PQXsR9LgQ@jetpack": {minVersion: "0.2.1", installs: 177},
+ "_6oMembers_@free.heroicplay.com": {minVersion: "7.38.8.46626", installs: 175},
+ "{4BBDD651-70CF-4821-84F8-2B918CF89CA3}": {minVersion: "8.9.3.1", installs: 173},
+ "jid1-GeRCnsiDhZiTvA@jetpack": {minVersion: "1.0.3", installs: 172},
+ "jid0-zs24wecdcQo0Lp18D7QOV4WSZFo@jetpack": {minVersion: "0.2.1-signed.1-signed", installs: 171},
+ "{c50ca3c4-5656-43c2-a061-13e717f73fc8}": {minVersion: "5.0.1.48.1-signed.1-signed", installs: 170},
+ "selenium_ide_buttons@egarracingteam.com.ar": {minVersion: "1.2.0.1-signed.1-signed", installs: 170},
+ "WSVCU@Wondershare.com": {minVersion: "7.1.0", installs: 169},
+ "{4cc4a13b-94a6-7568-370d-5f9de54a9c7f}": {minVersion: "2.7.1-signed.1-signed", installs: 168},
+ "{aa84ce40-4253-a00a-8cd6-0800200f9a67}": {minVersion: "3.12.0", installs: 168},
+ "FasterFox_Lite@BigRedBrent": {minVersion: "3.9.9Lite.1-signed.1-signed", installs: 167},
+ "{6cc0f0f7-a6e2-4834-9682-24de2229b51e}": {minVersion: "23.6", installs: 166},
+ "{b749fc7c-e949-447f-926c-3f4eed6accfe}": {minVersion: "0.7.1.1.1-signed.1-signed", installs: 166},
+ "@mendeleyimporter": {minVersion: "1.6.8", installs: 166},
+ "ALone-live@ya.ru": {minVersion: "1.4.11", installs: 166},
+ "{4093c4de-454a-4329-8aff-c6b0b123c386}": {minVersion: "0.8.14.1-signed.1-signed", installs: 165},
+ "cookiemgr@jayapal.com": {minVersion: "5.12", installs: 164},
+ "touchenex@raon.co.kr": {minVersion: "1.0.1.11", installs: 163},
+ "{b0e1b4a6-2c6f-4e99-94f2-8e625d7ae255}": {minVersion: "3.5.0.1-signed.1-signed", installs: 162},
+ "isreaditlater@ideashower.com": {minVersion: "3.0.6.1-signed", installs: 161},
+ "safesearchplus@avira.com": {minVersion: "1.4.1.371", installs: 161},
+ "_e0Members_@www.downshotfree.com": {minVersion: "7.102.10.3833", installs: 159},
+ "LDSI_plashcor@gmail.com": {minVersion: "1.1.0.3", installs: 159},
+ "jid1-9ETkKdBARv7Iww@jetpack": {minVersion: "0.20.1-signed.1-signed", installs: 157},
+ "jid1-CGxMej0nDJTjwQ@jetpack": {minVersion: "1.0.1-signed.1-signed", installs: 157},
+ "{00f7ab9f-62f4-4145-b2f9-38d579d639f6}": {minVersion: "49", installs: 156},
+ "googledictionary@toptip.ca": {minVersion: "7.5", installs: 156},
+ "shopearn@prodege.com": {minVersion: "219", installs: 156},
+ "fvdmedia@gmail.com": {minVersion: "11.0.1", installs: 155},
+ "magicplayer_unlisted@acestream.org": {minVersion: "1.1.42", installs: 155},
+ "{0538E3E3-7E9B-4d49-8831-A227C80A7AD3}": {minVersion: "2.2.2.1-signed.1-let-fixed.1-signed", installs: 154},
+ "{73007fef-a6e0-47d3-b4e7-dfc116ed6f65}": {minVersion: "1.15.1-signed.1-signed", installs: 153},
+ "{cd617372-6743-4ee4-bac4-fbf60f35719e}": {minVersion: "2.0.1-signed.1-signed", installs: 152},
+ "TopSecurityTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 152},
+ "jid1-hDf2iQXGiUjzGQ@jetpack": {minVersion: "2.5.0", installs: 151},
+ "_dnMembers_@www.free.webmailworld.com": {minVersion: "7.102.10.5052", installs: 149},
+ "jid1-rrMTK7JqsxNOeQ@jetpack": {minVersion: "2.1.0", installs: 149},
+ "jid1-XgC5trUcILmXBw@jetpack": {minVersion: "2.0.3", installs: 149},
+ "online_banking_69A4E213815F42BD863D889007201D82@kaspersky.com": {minVersion: "4.5.3.8", installs: 148},
+ "jid1-AXn9cXcB4fD1QQ@jetpack": {minVersion: "0.7.4", installs: 148},
+ "feedly@devhd": {minVersion: "16.0.528.1-signed.1-signed", installs: 147},
+ "{6E727987-C8EA-44DA-8749-310C0FBE3C3E}": {minVersion: "2.0.0.11", installs: 146},
+ "{1082eb84-f0f2-11e5-8e18-9bb85ab7992e}": {minVersion: "1.07", installs: 146},
+ "{c151d79e-e61b-4a90-a887-5a46d38fba99}": {minVersion: "2.8.8", installs: 146},
+ "public.proartex@gmail.com": {minVersion: "1.1.3", installs: 145},
+ "jid1-8J7ayxTha4KqKQ@jetpack": {minVersion: "1.1.1-signed.1-signed", installs: 144},
+ "stealthyextension@gmail.com": {minVersion: "3.0.1.1-signed", installs: 144},
+ "_beMembers_@free.dailylocalguide.com": {minVersion: "7.38.9.7920", installs: 143},
+ "mytube@ashishmishra.in": {minVersion: "0.979.1-signed.1-signed", installs: 142},
+ "@A3592ADB-854A-443A-854E-EB92130D470D": {minVersion: "1.08.8.88", installs: 139},
+ "FunkyTVTabs-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 139},
+ "jid1-QpHD8URtZWJC2A@jetpack": {minVersion: "4.3.0", installs: 138},
+ "savedeo-video-downloader@fczbkk.com": {minVersion: "0.4.1.1-signed.1-signed", installs: 137},
+ "toolbar@shopathome.com": {minVersion: "8.20.3.1", installs: 137},
+ "_dyMembers_@www.dezipper.com": {minVersion: "7.102.10.3775", installs: 135},
+ "jid0-zXo3XFGyiDalgkeEO4UYJTUwo2I@jetpack": {minVersion: "1.0.0", installs: 134},
+ "{d57c9ff1-6389-48fc-b770-f78bd89b6e8a}": {minVersion: "1.46.1-signed.1-signed", installs: 133},
+ "@searchlock-fx": {minVersion: "1.1.6", installs: 133},
+ "dm@jetpack": {minVersion: "0.0.2", installs: 133},
+ "proxyselector@mozilla.org": {minVersion: "1.31.1-signed.1-signed", installs: 133},
+ "{065829BC-17B5-4C0B-9429-3173C361092E}": {minVersion: "1.0.8", installs: 132},
+ "{ada4b710-8346-4b82-8199-5de2b400a6ae}": {minVersion: "2.1.5.5.3", installs: 132},
+ "readable@evernote.com": {minVersion: "10.2.1.7.1-signed", installs: 131},
+ "{d48a39ba-8f80-4fce-8ee1-bc710561c55d}": {minVersion: "3.1.0.1-signed.1-signed", installs: 131},
+ "autorefresh@plugin": {minVersion: "1.0.2.1-signed.1-signed", installs: 130},
+ "SafeBrowseSearch-the-extension1@mozilla.com": {minVersion: "0.1.2", installs: 130},
+ "browsermodulecorp@browcorporation.org": {minVersion: "2.3", installs: 129},
+ "wisestamp@wisestamp.com": {minVersion: "4.14.20", installs: 127},
+ "_63Members_@www.aplusgamer.com": {minVersion: "7.38.8.45832", installs: 126},
+ "bestproxyswitcher@bestproxyswitcher.com": {minVersion: "3.4.6.1-signed.1-signed", installs: 126},
+ "jid1-AVgCeF1zoVzMjA@jetpack": {minVersion: "0.9.5.6", installs: 126},
+ "{ce7e73df-6a44-4028-8079-5927a588c948}": {minVersion: "1.1.4", installs: 125},
+ "{E71B541F-5E72-5555-A47C-E47863195841}": {minVersion: "3.0.3", installs: 125},
+ "{F5DDF39C-9293-4d5e-9AA8-E04E6DD5E9B4}": {minVersion: "1.6.3.1-signed.1-signed", installs: 125},
+ "@simplepopupblocker": {minVersion: "1.2.1", installs: 125},
+ "commonfix@mozillaonline.com": {minVersion: "0.13", installs: 125},
+ "searchme@mybrowserbar.com": {minVersion: "2.8", installs: 125},
+ "_4wMembers_@www.retrogamer.com": {minVersion: "7.38.8.46604", installs: 124},
+ "{71328583-3CA7-4809-B4BA-570A85818FBB}": {minVersion: "0.8.6.3.1-let-fixed", installs: 123},
+ "dmremote@westbyte.com": {minVersion: "1.9.3", installs: 123},
+ "@google-translate-menu": {minVersion: "1.0.1", installs: 122},
+ "_aaMembers_@free.eliteunzip.com": {minVersion: "7.39.8.50909", installs: 121},
+ "{8620c15f-30dc-4dba-a131-7c5d20cf4a29}": {minVersion: "3.9", installs: 121},
+ "{eb4b28c8-7f2d-4327-a00c-40de4299ba44}": {minVersion: "1.7", installs: 121},
+ "flashlight@stephennolan.com.au": {minVersion: "1.2.1-signed.1-signed", installs: 121},
+ "useragentoverrider@qixinglu.com": {minVersion: "0.4.1", installs: 121},
+ "{1B33E42F-EF14-4cd3-B6DC-174571C4349C}": {minVersion: "4.7", installs: 120},
+ "_dxMembers_@www.download-freemaps.com": {minVersion: "7.38.8.46371", installs: 120},
+ "{95ab36d4-fb6f-47b0-8b8d-e5f3bd547953}": {minVersion: "4.20.13.1-signed.1-signed", installs: 120},
+ "FirefoxAddon@similarWeb.com": {minVersion: "4.0.6", installs: 120},
+ "flashstopper@byo.co.il": {minVersion: "1.4.2", installs: 120},
+ "{15e67a59-bd3d-49ae-90dd-b3d3fd14c2ed}": {minVersion: "1.0.3.1-signed.1-signed", installs: 119},
+ "{c37bac34-849a-4d28-be41-549b2c76c64e}": {minVersion: "2.6", installs: 119},
+ "{03B08592-E5B4-45ff-A0BE-C1D975458688}": {minVersion: "1.1.1-signed.1-signed", installs: 118},
+ "newtabgoogle@graememcc.co.uk": {minVersion: "1.0.2.1-signed.1-signed", installs: 118},
+ "SocialNewtabs-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 118},
+ "@kikikokicicidada": {minVersion: "2.1.2", installs: 117},
+ "{9D6218B8-03C7-4b91-AA43-680B305DD35C}": {minVersion: "4.0.5", installs: 116},
+ "extension@one-tab.com": {minVersion: "1.17.0", installs: 116},
+ "{22119944-ED35-4ab1-910B-E619EA06A115}": {minVersion: "7.9.21.5", installs: 115},
+ "admin@hide-my-ip.org": {minVersion: "9.6.3", installs: 115},
+ "bdwteffv19@bitdefender.com": {minVersion: "2.2.1", installs: 115},
+ "exif_viewer@mozilla.doslash.org": {minVersion: "2.00.1-signed.1-signed", installs: 115},
+ "MyStartab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 115},
+ "coralietab@mozdev.org": {minVersion: "2.04.20110724.1-signed.1-signed", installs: 113},
+ "gaurangnshah@gmail.com": {minVersion: "1.3.2.1-signed.1-signed", installs: 113},
+ "ImagePicker@topolog.org": {minVersion: "1.9.4", installs: 113},
+ "{d49a148e-817e-4025-bee3-5d541376de3b}": {minVersion: "3.1.1-signed.1-signed", installs: 112},
+ "firebug@tools.sitepoint.com": {minVersion: "1.6.1-signed.1-signed", installs: 111},
+ "add-to-searchbox@maltekraus.de": {minVersion: "2.9", installs: 110},
+ "captiondownloader@hiephm.com": {minVersion: "2.3.1-signed.1-signed", installs: 110},
+ "jid1-LYopfl0r00ZV5k@jetpack": {minVersion: "1.0.1-signed.1-signed", installs: 110},
+ "{7CA9CF31-1C73-46CD-8377-85AB71EA771F}": {minVersion: "5.0.12", installs: 109},
+ "jid1-HdwPLukcGQeOSh@jetpack": {minVersion: "1.2.3", installs: 108},
+ "{0AA9101C-D3C1-4129-A9B7-D778C6A17F82}": {minVersion: "2.09.1-signed", installs: 107},
+ "CookiesIE@yahoo.com": {minVersion: "1.0.1-signed.1-signed", installs: 107},
+ "selenium-expert_selenium-ide@Samit.Badle": {minVersion: "0.25.1-signed.1-signed", installs: 107},
+ "{19EB90DC-A456-458b-8AAC-616D91AAFCE1}": {minVersion: "1.0.1-signed", installs: 105},
+ "application@itineraire.info": {minVersion: "4.5.0", installs: 105},
+ "rest-easy@quickmediasolutions.com": {minVersion: "0.3.1.1-signed", installs: 105},
+ "TopSocialHub-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 105},
+ "{7affbfae-c4e2-4915-8c0f-00fa3ec610a1}": {minVersion: "6.36.32", installs: 104},
+ "azhang@cloudacl.com": {minVersion: "0.19.6.9.1-signed.1-signed", installs: 104},
+ "FunCyberTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 104},
+ "SkipScreen@SkipScreen": {minVersion: "0.7.2.1-signed.1-signed", installs: 104},
+ "toolbar@seomoz.org": {minVersion: "3.1.18", installs: 104},
+ "{8b86149f-01fb-4842-9dd8-4d7eb02fd055}": {minVersion: "0.26.1-signed.1-signed", installs: 103},
+ "fbp@fbpurity.com": {minVersion: "9.3.2.1-signed", installs: 103},
+ "jid1-V8ev2melBDV3qQ@jetpack": {minVersion: "1.0.12.1-signed.1-signed", installs: 103},
+ "_fvMembers_@free.directionsace.com": {minVersion: "7.102.10.3790", installs: 102},
+ "{b6b1a201-b252-484f-b9fe-68efbb273fbd}": {minVersion: "1.10.1-signed.1-signed", installs: 102},
+ "flashfirebug@o-minds.com": {minVersion: "4.9.1", installs: 102},
+ "_ebMembers_@download.metrohotspot.com": {minVersion: "7.102.10.4735", installs: 101},
+ "{2e17e2b2-b8d4-4a67-8d7b-fafa6cc9d1d0}": {minVersion: "1.2.7.0.1-signed.1-signed", installs: 101},
+ "{ea61041c-1e22-4400-99a0-aea461e69d04}": {minVersion: "0.2.4.1-signed.1-signed", installs: 101},
+ "rapportive@rapportive.com": {minVersion: "1.4.0.1.1-signed.1-signed", installs: 101},
+ "_dvMembers_@www.testinetspeed.com": {minVersion: "7.38.8.45918", installs: 100},
+ "{9aad3da6-6c46-4ef0-9109-6df5eaaf597c}": {minVersion: "1.4.1.1-signed.1-signed", installs: 100},
+ "{c2b1f3ae-5cd5-49b7-8a0c-2c3bcbbbb294}": {minVersion: "1.1.1-signed.1-signed", installs: 100},
+ "jid0-w1UVmoLd6VGudaIERuRJCPQx1dQ@jetpack": {minVersion: "1.6.8.1-signed", installs: 100},
+ "_cxMembers_@www.autopcbackup.com": {minVersion: "7.102.10.3597", installs: 99},
+ "vpn@hide-my-ip.org": {minVersion: "10.6.2", installs: 99},
+ "{1a5dabbd-0e74-41da-b532-a364bb552cab}": {minVersion: "1.0.9.1-signed", installs: 98},
+ "FirePHPExtension-Build@firephp.org": {minVersion: "0.7.4.1-signed.1-signed", installs: 98},
+ "jid1-UXDr6c69BeyPVw@jetpack": {minVersion: "0.8.2", installs: 98},
+ "TopSafeTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 98},
+ "{3b56bcc7-54e5-44a2-9b44-66c3ef58c13e}": {minVersion: "0.9.7.4", installs: 97},
+ "autoreload@yz.com": {minVersion: "1.21", installs: 97},
+ "manish.p05@gmail.com": {minVersion: "12.9", installs: 97},
+ "videoresumer@jetpack": {minVersion: "1.1.4", installs: 97},
+ "@Radio": {minVersion: "0.2.0", installs: 96},
+ "_hfMembers_@free.everydaymanuals.com": {minVersion: "7.102.10.4142", installs: 96},
+ "jid0-jJRRRBMgoShUhb07IvnxTBAl29w@jetpack": {minVersion: "2.0.4", installs: 96},
+ "rikaichan-jpen@polarcloud.com": {minVersion: "2.01.160101", installs: 96},
+ "{7c6cdf7c-8ea8-4be7-ae5a-0b3effe14d66}": {minVersion: "49.1", installs: 95},
+ "{FDBAD97E-A258-4fe3-9CF6-60CF386C4422}": {minVersion: "2.0.1.6", installs: 95},
+ "intgcal@egarracingteam.com.ar": {minVersion: "1.5.1", installs: 95},
+ "MediaNewTab-the-extension1@mozilla.com": {minVersion: "0.1.6", installs: 95},
+ "{9EB34849-81D3-4841-939D-666D522B889A}": {minVersion: "2.4.0.157", installs: 94},
+ "{158d7cb3-7039-4a75-8e0b-3bd0a464edd2}": {minVersion: "2.7.1-signed.1-signed", installs: 94},
+ "jid1-ach2kaGSshPJCg@jetpack": {minVersion: "0.1.1-signed.1-signed", installs: 94},
+ "jid1-cwbvBTE216jjpg@jetpack": {minVersion: "2.1.0.1-signed.1-signed", installs: 94},
+ "{f36c6cd1-da73-491d-b290-8fc9115bfa55}": {minVersion: "3.0.9.1-signed.1-let-fixed.1-signed", installs: 93},
+ "dmpluginff@westbyte.com": {minVersion: "1.4.12", installs: 93},
+ "firefox@serptrends.com": {minVersion: "0.8.14", installs: 93},
+ "panel-plugin@effectivemeasure.com": {minVersion: "4.0.0", installs: 93},
+ "_evMembers_@www.free.bestbackground.com": {minVersion: "7.102.10.3607", installs: 92},
+ "canitbecheaper@trafficbroker.co.uk": {minVersion: "3.9.78", installs: 92},
+ "favorites_selenium-ide@Samit.Badle": {minVersion: "2.0.1-signed.1-signed", installs: 92},
+ "{5F590AA2-1221-4113-A6F4-A4BB62414FAC}": {minVersion: "0.45.8.20130519.3.1-signed.1-signed", installs: 90},
+ "{3e9bb2a7-62ca-4efa-a4e6-f6f6168a652d}": {minVersion: "2.7.7.1-signed.1-signed", installs: 90},
+ "{ab4b5718-3998-4a2c-91ae-18a7c2db513e}": {minVersion: "1.2.0.1-signed.1-signed", installs: 90},
+ "2020Player_WEB@2020Technologies.com": {minVersion: "5.0.94.0", installs: 90},
+ "translator@dontfollowme.net": {minVersion: "2.0.5", installs: 90},
+ "YouTubeAutoReplay@arikv.com": {minVersion: "3.3.1-signed.1-signed", installs: 90},
+ "{a949831f-d9c0-45ae-8c60-91c2a86fbfb6}": {minVersion: "0.2.1-signed.1-signed", installs: 89},
+ "@vpn-unlimited-secure-proxy": {minVersion: "4.4", installs: 89},
+ "jid1-JcGokIiQyjoBAQ@jetpack": {minVersion: "0.6.1-signed.1-signed", installs: 89},
+ "_73Members_@www.easyhomedecorating.com": {minVersion: "7.102.10.4129", installs: 88},
+ "{065ee92a-ad57-42a2-b6d5-466b6fd8e24d}": {minVersion: "0.11.6.1-signed.1-signed", installs: 88},
+ "{455D905A-D37C-4643-A9E2-F6FEFAA0424A}": {minVersion: "0.8.17.1-signed.1-signed", installs: 88},
+ "{7eb3f691-25b4-4a85-9038-9e57e2bcd537}": {minVersion: "0.4.4.1-signed.1-signed", installs: 88},
+ "FunSocialTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 88},
+ "Lucifox@lucidor.org": {minVersion: "0.9.13", installs: 88},
+ "YourMediaTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 88},
+ "youtube-video-player@lejenome.me": {minVersion: "0.2.38.1-signed.1-signed", installs: 88},
+ "_hgMembers_@free.atozmanuals.com": {minVersion: "7.102.10.3604", installs: 87},
+ "abb-acer@amazon.com": {minVersion: "10.161.13.1002", installs: 87},
+ "gmail_panel@alejandrobrizuela.com.ar": {minVersion: "1.2.0", installs: 87},
+ "izer@camelcamelcamel.com": {minVersion: "2.8.2", installs: 87},
+ "tvnewtab-the-extension1@mozilla.com": {minVersion: "0.1.5", installs: 87},
+ "vlc_shortcut@kosan.kosan": {minVersion: "0.8.3.0", installs: 87},
+ "youtubeunblocker@unblocker.yt": {minVersion: "0.6.20", installs: 86},
+ "email@jetpack": {minVersion: "0.0.16", installs: 86},
+ "extensions@gismeteo.com": {minVersion: "5.1.0.2", installs: 86},
+ "idaremote@westbyte.com": {minVersion: "1.6.3", installs: 86},
+ "{725fc0a6-1f6b-4cf9-ae17-748d111dc16d}": {minVersion: "1.1.0", installs: 85},
+ "jid1-461B0PwxL3oTt1@jetpack": {minVersion: "0.2.1-signed.1-signed", installs: 85},
+ "webdavlauncher@benryan.com": {minVersion: "1.1.0", installs: 85},
+ "jid1-ZM3BerwS6FsQAg@jetpack": {minVersion: "0.4.1-signed", installs: 84},
+ "_fwMembers_@free.howtosuite.com": {minVersion: "7.102.10.4280", installs: 84},
+ "{023e9ca0-63f3-47b1-bcb2-9badf9d9ef28}": {minVersion: "4.4.3.1-signed.1-signed", installs: 84},
+ "{25A1388B-6B18-46c3-BEBA-A81915D0DE8F}": {minVersion: "1.7.8.5.1-signed.1-signed", installs: 84},
+ "{75493B06-1504-4976-9A55-B6FE240FF0BF}": {minVersion: "3.4.0.0", installs: 84},
+ "facepaste.firefox.addon@azabani.com": {minVersion: "2.91", installs: 84},
+ "jid1-cplLTTY501TB2Q@jetpack": {minVersion: "0.5.1", installs: 84},
+ "_d1Members_@free.mysocialshortcut.com": {minVersion: "7.102.10.4792", installs: 83},
+ "{761a54f1-8ccf-4112-9e48-dbf72adf6244}": {minVersion: "2.3.1-signed.1-signed", installs: 83},
+ "{BBB77B49-9FF4-4d5c-8FE2-92B1D6CD696C}": {minVersion: "2.0.0.1083", installs: 83},
+ "{a3a5c777-f583-4fef-9380-ab4add1bc2a2}": {minVersion: "2.1.4", installs: 82},
+ "{eb80b076-a444-444c-a590-5aee5d977d80}": {minVersion: "2.6.18", installs: 82},
+ "KVAllmytube@KeepVid.com": {minVersion: "4.10.0", installs: 82},
+ "lumerias-instagram@lumerias.com": {minVersion: "1.3", installs: 82},
+ "omnibar@ajitk.com": {minVersion: "0.7.28.20141004.1-signed.1-signed", installs: 81},
+ "@autofillanyforms-easytatkal": {minVersion: "7.51.0", installs: 81},
+ "@youtuberightclick": {minVersion: "0.0.3", installs: 81},
+ "autoproxy@autoproxy.org": {minVersion: "0.4b2.2013051811.1-signed.1-let-fixed.1-signed", installs: 80},
+ "{e33788ea-0bb9-4502-9c77-bdc551afc8ad}": {minVersion: "1.0.4", installs: 80},
+ "dmmm@westbyte.com": {minVersion: "1.3.4", installs: 80},
+ "easycopy@smokyink.com": {minVersion: "2.7.0", installs: 80},
+ "jid1-LelsJ0Oz0rt71A@jetpack": {minVersion: "2.0.0", installs: 80},
+ "_f7Members_@download.smsfrombrowser.com": {minVersion: "7.38.8.45917", installs: 79},
+ "{6614d11d-d21d-b211-ae23-815234e1ebb5}": {minVersion: "3.9.13", installs: 79},
+ "FunTvTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 79},
+ "{4204c864-50bf-467a-95b3-0912b7f15869}": {minVersion: "1.2.00.1-signed.1-signed", installs: 78},
+ "{987311C6-B504-4aa2-90BF-60CC49808D42}": {minVersion: "3.1-signed.1-signed", installs: 78},
+ "uploader@adblockfilters.mozdev.org": {minVersion: "2.1.1-signed.1-let-fixed.1-signed", installs: 77},
+ "PageRank@addonfactory.in": {minVersion: "2.0.1-signed.1-signed", installs: 77},
+ "restartbutton@strk.jp": {minVersion: "0.1.5.1-signed.1-signed", installs: 77},
+ "text2voice@vik.josh": {minVersion: "1.15", installs: 77},
+ "_dpMembers_@free.findyourmaps.com": {minVersion: "7.102.10.4185", installs: 76},
+ "53ffxtbr@www.dailyfitnesscenter.com": {minVersion: "7.36.8.15623", installs: 76},
+ "gary@builtwith.com": {minVersion: "1.9.6.1-signed.1-signed", installs: 76},
+ "idamm@westbyte.com": {minVersion: "1.3.2", installs: 76},
+ "jid1-3gu11JeYBiIuJA@jetpack": {minVersion: "3.1.1", installs: 76},
+ "jid1-zV8eHYwTDNUtwQ@jetpack": {minVersion: "1.0.4", installs: 76},
+ "nst@neiron.ru": {minVersion: "7.3.0.2", installs: 76},
+ "service@touchpdf.com": {minVersion: "1.15.1-signed.1-signed", installs: 76},
+ "{02450954-cdd9-410f-b1da-db804e18c671}": {minVersion: "0.96.3.1-signed.1-signed", installs: 75},
+ "{4176DFF4-4698-11DE-BEEB-45DA55D89593}": {minVersion: "0.8.50.1-signed.1-signed", installs: 75},
+ "{DAD0F81A-CF67-4eed-98D6-26F6E47274CA}": {minVersion: "1.8.1-signed.1-signed", installs: 75},
+ "dealxplorermysites770@yahoo.com": {minVersion: "0.0.0.1", installs: 75},
+ "firefox@online-convert.com": {minVersion: "1.4.1-signed.1-signed", installs: 75},
+ "jid1-zmgYgiQPXJtjNA@jetpack": {minVersion: "1.23", installs: 75},
+ "_evMembers_@www.bestbackground.com": {minVersion: "7.38.9.7654", installs: 74},
+ "dmbarff@westbyte.com": {minVersion: "1.5.11", installs: 74},
+ "inf@youtube-mp3.video": {minVersion: "0.1", installs: 74},
+ "jid1-e7w0SHT82Gv9qA@jetpack": {minVersion: "1.2", installs: 74},
+ "NewTabTVCool-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 74},
+ "{E173B749-DB5B-4fd2-BA0E-94ECEA0CA55B}": {minVersion: "7.4.1-signed", installs: 73},
+ "{9BAE5926-8513-417d-8E47-774955A7C60D}": {minVersion: "1.1.1d.1-signed.1-signed", installs: 73},
+ "{3cc6c6ba-654c-417e-a8af-6997ac388ae1}": {minVersion: "49", installs: 72},
+ "{daf44bf7-a45e-4450-979c-91cf07434c3d}": {minVersion: "2.0.5", installs: 72},
+ "{dbac9680-d559-4cd4-9765-059879e8c467}": {minVersion: "5.0.5", installs: 72},
+ "application@recettes.net": {minVersion: "4.5.0", installs: 72},
+ "idapluginff@westbyte.com": {minVersion: "1.5.9", installs: 71},
+ "imgflashblocker@shimon.chohen": {minVersion: "0.7.1-signed.1-signed", installs: 71},
+ "inspector@mozilla.org": {minVersion: "2.0.16.1-signed", installs: 71},
+ "jid1-ReWlW1efOwaQJQ@jetpack": {minVersion: "1.1.2", installs: 71},
+ "youtubedownloader@trafficterminal.com": {minVersion: "1.0.1.1-signed.1-signed", installs: 71},
+ "FavIconReloader@mozilla.org": {minVersion: "0.8.1-signed", installs: 70},
+ "_2bMembers_@www.bettercareersearch.com": {minVersion: "7.38.8.45828", installs: 70},
+ "{5e594888-3e8e-47da-b2c6-b0b545112f84}": {minVersion: "1.3.18", installs: 70},
+ "@greatdealzu": {minVersion: "0.0.3", installs: 70},
+ "86ffxtbr@download.yourvideochat.com": {minVersion: "7.36.8.15938", installs: 70},
+ "google@hitachi.com": {minVersion: "0.3.1-signed.1-signed", installs: 70},
+ "{6e84150a-d526-41f1-a480-a67d3fed910d}": {minVersion: "1.5.6.1-signed.1-signed", installs: 69},
+ "firepicker@thedarkone": {minVersion: "1.4.3.1-signed.1-signed", installs: 69},
+ "jid0-AocRXUCRsLTCYvn6bgJERnwfuqw@jetpack": {minVersion: "2.8.3.1-signed.1-signed", installs: 69},
+ "nortonsecurity@symantec.com": {minVersion: "7.2.0f90", installs: 69},
+ "{ef4e370e-d9f0-4e00-b93e-a4f274cfdd5a}": {minVersion: "1.4.10.1-signed", installs: 68},
+ "{d4e0dc9c-c356-438e-afbe-dca439f4399d}": {minVersion: "49.1", installs: 68},
+ "{E6C93316-271E-4b3d-8D7E-FE11B4350AEB}": {minVersion: "2.1.25.1-signed.1-signed", installs: 68},
+ "{fa8476cf-a98c-4e08-99b4-65a69cb4b7d4}": {minVersion: "1.7.6.1", installs: 68},
+ "simplesiteblocker@example.com": {minVersion: "1.1.1-signed.1-signed", installs: 68},
+ "_fpMembers_@free.passwordlogic.com": {minVersion: "7.102.10.4853", installs: 67},
+ "{6e764c17-863a-450f-bdd0-6772bd5aaa18}": {minVersion: "1.0.3.1-signed.1-signed", installs: 67},
+ "adbeaver@adbeaver.org": {minVersion: "0.7.2.9", installs: 67},
+ "application2@allo-pages.fr": {minVersion: "4.5.0", installs: 67},
+ "arf3@getcartt.com": {minVersion: "1.2.3", installs: 67},
+ "clearcache@michel.de.almeida": {minVersion: "2.0.1.1-signed.1-signed", installs: 67},
+ "fbmessengerpanel@alejandrobrizuela.com.ar": {minVersion: "1.0.3.1-signed.1-signed", installs: 67},
+ "tilt@mozilla.com": {minVersion: "1.0.1.1-signed.1-signed", installs: 67},
+ "toolbar_AVIRA-V7@apn.ask.com": {minVersion: "127.25", installs: 67},
+ "{524B8EF8-C312-11DB-8039-536F56D89593}": {minVersion: "4.91.0.0", installs: 66},
+ "{9d1f059c-cada-4111-9696-41a62d64e3ba}": {minVersion: "0.17.0.1", installs: 66},
+ "{B068AC18-0121-4e67-9A7E-6386F93F4F7A}": {minVersion: "2.4", installs: 66},
+ "@lottadealsun": {minVersion: "0.0.1", installs: 66},
+ "@thebestyoutubedownloader": {minVersion: "1.0.0.7", installs: 66},
+ "TopSocialTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 66},
+ "{11483926-db67-4190-91b1-ef20fcec5f33}": {minVersion: "0.4.9.1", installs: 65},
+ "{8AA36F4F-6DC7-4c06-77AF-5035170634FE}": {minVersion: "2016.9.16", installs: 65},
+ "dam@tensons.com": {minVersion: "5.0.7", installs: 65},
+ "jid1-D7momAzRw417Ag@jetpack": {minVersion: "4.5.13", installs: 65},
+ "support@videoadd.ru": {minVersion: "2.8.1.1-signed.1-signed", installs: 65},
+ "{95322c08-05ff-4f3c-85fd-8ceb821988dd}": {minVersion: "49", installs: 64},
+ "AllMyTube@Wondershare.com": {minVersion: "4.9.1", installs: 64},
+ "azan-times@hamid.net": {minVersion: "1.2.3.1-signed.1-signed", installs: 64},
+ "check-compatibility@dactyl.googlecode.com": {minVersion: "1.3.1-signed.1-signed", installs: 64},
+ "ifamebook@stormvision.it": {minVersion: "4.03.1-signed", installs: 64},
+ "jid1-vRJA7N8VwBoiXw@jetpack": {minVersion: "1.1.1.1-signed", installs: 64},
+ "{04426594-bce6-4705-b811-bcdba2fd9c7b}": {minVersion: "1.7.1-signed.1-signed", installs: 63},
+ "{f3f219f9-cbce-467e-b8fe-6e076d29665c}": {minVersion: "50", installs: 63},
+ "fireforce@scrt.ch": {minVersion: "2.2.1-signed.1-signed", installs: 63},
+ "FunkyMediaTab-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 63},
+ "TopTabTV-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 63},
+ "{018f3160-1a6f-4650-84fd-aad8c13609c8}": {minVersion: "0.1.1-signed.1-signed", installs: 62},
+ "{2e710e6b-5e9d-44ba-8f4e-09a040978b49}": {minVersion: "1.7", installs: 62},
+ "{c1970c0d-dbe6-4d91-804f-c9c0de643a57}": {minVersion: "1.3.2.13.1-signed.1-signed", installs: 62},
+ "{c9b4529a-eeba-4e48-976e-f3d3f9026e04}": {minVersion: "1.1.1-signed.1-signed", installs: 62},
+ "{df4e4df5-5cb7-46b0-9aef-6c784c3249f8}": {minVersion: "1.3.0.1-signed.1-signed", installs: 62},
+ "@stremio": {minVersion: "1.0.2", installs: 62},
+ "application@les-pages.com": {minVersion: "4.4.0", installs: 62},
+ "ffvkontaktemusic@chupakabr.ru": {minVersion: "2.2.1-signed.1-signed", installs: 62},
+ "firefinder@robertnyman.com": {minVersion: "1.4.1-signed.1-signed", installs: 62},
+ "formhistory@yahoo.com": {minVersion: "1.4.0.6", installs: 62},
+ "fxclickonce@rushyo.com": {minVersion: "0.1.1-signed.1-signed", installs: 62},
+ "gmail@borsosfisoft.com": {minVersion: "1.0.1.1-signed.1-signed", installs: 62},
+ "HighlightedTextToFile@bobbyrne01.org": {minVersion: "2.7.1", installs: 62},
+ "jid1-n85lxPv1NAWVTQ@jetpack": {minVersion: "0.96.1.1-signed", installs: 62},
+ "ramback@pavlov.net": {minVersion: "1.0.1-signed.1-signed", installs: 62},
+ "VacuumPlacesImproved@lultimouomo-gmail.com": {minVersion: "1.2.1-signed.1-signed", installs: 62},
+ "@News": {minVersion: "0.2.0", installs: 61},
+ "{45d8ff86-d909-11db-9705-005056c00008}": {minVersion: "1.3.4.8", installs: 61},
+ "{686fc9c5-c339-43db-b93a-5181a217f9a6}": {minVersion: "1.11", installs: 61},
+ "{ea2b95c2-9be8-48ed-bdd1-5fcd2ad0ff99}": {minVersion: "0.3.8.1.1-signed.1-signed", installs: 61},
+ "@chomikuj": {minVersion: "1.2.0", installs: 61},
+ "avg@wtu3": {minVersion: "3.7.0.0", installs: 61},
+ "jid1-f7dnBeTj8ElpWQ@jetpack": {minVersion: "1.34.1-signed.1-signed", installs: 61},
+ "jid1-OY8Xu5BsKZQa6A@jetpack": {minVersion: "2.0.21", installs: 61},
+ "jid1-u9RbFp9JcoEGGw@jetpack": {minVersion: "1.2.2.1-signed.1-signed", installs: 61},
+ "plugin@okta.com": {minVersion: "5.8.0", installs: 61},
+ "showpassword@pratikpoddar": {minVersion: "1.7.1-signed.1-signed", installs: 61},
+ "IGF.F3@igufei.com": {minVersion: "3.2.11", installs: 60},
+ "{12b6fdcd-4423-4276-82a3-73fdbff5f7e4}": {minVersion: "50", installs: 60},
+ "{8F6A6FD9-0619-459f-B9D0-81DE065D4E21}": {minVersion: "1.13", installs: 60},
+ "jid1-mW7iuA66Ny8Ziw@jetpack": {minVersion: "0.9.1-signed.1-signed", installs: 60},
+ "nishan.naseer.googimagesearch@gmail.com": {minVersion: "0.5.1-signed.1-signed", installs: 60},
+ "quicksearch@yandex.ru": {minVersion: "1.0.13", installs: 60},
+ "{902D2C4A-457A-4EF9-AD43-7014562929FF}": {minVersion: "0.6.4", installs: 59},
+ "@yset": {minVersion: "0.0.10", installs: 59},
+ "csscoverage@spaghetticoder.org": {minVersion: "0.3.4.1-signed.1-signed", installs: 59},
+ "dgnria2@nuance.com": {minVersion: "15.00.000.058", installs: 59},
+ "firequery@binaryage.com": {minVersion: "2.0.4", installs: 59},
+ "IBM-cck@firefox-extensions.ibm.com": {minVersion: "2.3.0", installs: 59},
+ "trackmenot@mrl.nyu.edu": {minVersion: "0.9.2", installs: 59},
+ "_chMembers_@free.discoverancestry.com": {minVersion: "7.102.10.3818", installs: 58},
+ "{338e0b96-2285-4424-b4c8-e25560750fa3}": {minVersion: "3.1-signed.1-signed", installs: 58},
+ "{8b5bea8c-6194-4c7c-a440-d5ca181480c3}": {minVersion: "1.500.000.11", installs: 58},
+ "{e30e9060-21d5-11e3-8224-0800200c9a66}": {minVersion: "1.2.12", installs: 58},
+ "LDshowpicture_plashcor@gmail.com": {minVersion: "3.2", installs: 58},
+ "open.about.permissions@jasnapaka.com": {minVersion: "1.2.1-signed.1-signed", installs: 58},
+ "sqlime@security.compass": {minVersion: "0.4.7.1-signed.1-signed", installs: 58},
+ "@jetpack-easy-turism2": {minVersion: "7.1.0", installs: 57},
+ "check4change-owner@mozdev.org": {minVersion: "1.9.8.1", installs: 57},
+ "jid1-SDFC9fEAZRW7ab@jetpack": {minVersion: "0.1.3.1-signed.1-signed", installs: 57},
+ "linkgopher@oooninja.com": {minVersion: "1.3.3.1-signed.1-signed", installs: 57},
+ "pixelperfectplugin@openhouseconcepts.com": {minVersion: "2.0.14", installs: 57},
+ "YoutubeDownloader@huangho.net76.net": {minVersion: "1.6.5.1-signed.1-signed", installs: 57},
+ "lwthemes-manager@loucypher": {minVersion: "0.2.1-signed.1-let-fixed.1-signed", installs: 56},
+ "_eiMembers_@www.100sofrecipes.com": {minVersion: "7.102.10.3580", installs: 56},
+ "{068c594c-1a69-4f51-888d-1e231eac59a3}": {minVersion: "1", installs: 56},
+ "{139C4B80-60ED-11E4-80EC-84041E5D46B0}": {minVersion: "1.3", installs: 56},
+ "{4c7097f7-08f2-4ef2-9b9f-f95fa4cbb064}": {minVersion: "1.1", installs: 56},
+ "{776f38cb-6255-4b92-b5cf-e5c71ff2b688}": {minVersion: "1.6", installs: 56},
+ "{79c50f9a-2ffe-4ee0-8a37-fae4f5dacd4f}": {minVersion: "5.1.3", installs: 56},
+ "{8BCA0E8A-E57B-425b-A05B-CD3868EB577E}": {minVersion: "1.4.1-signed.1-signed", installs: 56},
+ "easycopypaste@everhelper.me": {minVersion: "1.1.0.1-signed.1-signed", installs: 56},
+ "NoiaFoxoption@davidvincent.tld": {minVersion: "3.0.2.1-signed", installs: 56},
+ "r2d2b2g@mozilla.org": {minVersion: "4.0.4.1-signed", installs: 56},
+ "TFToolbarX@torrent-finder": {minVersion: "1.3.1.1-signed.1-signed", installs: 56},
+ "{E4091D66-127C-11DB-903A-DE80D2EFDFE8}": {minVersion: "1.6.5.5.1-signed.1-signed", installs: 55},
+ "downintab@max.max": {minVersion: "1.00.1-signed.1-signed", installs: 55},
+ "flv2mp3@hotger.com": {minVersion: "2.3.2-signed", installs: 55},
+ "ISVCU@iSkysoft.com": {minVersion: "5.1.0", installs: 55},
+ "jid1-n5ARdBzHkUEdAA@jetpack": {minVersion: "3.8.7", installs: 55},
+ "rpnetdownloadhelper@gmail.com": {minVersion: "3.0.1-signed.1-signed", installs: 55},
+ "shpassword@shpassword.fr": {minVersion: "0.3.1-signed.1-signed", installs: 55},
+ "snt@simplenewtab.com": {minVersion: "1.3", installs: 55},
+ "admin@djamol.com": {minVersion: "4.31.1-signed.1-signed", installs: 54},
+ "{22870005-adef-4c9d-ae36-d0e1f2f27e5a}": {minVersion: "0.4.0.9.1.1-signed.1-signed", installs: 54},
+ "{DBBB3167-6E81-400f-BBFD-BD8921726F52}": {minVersion: "7125.2016.0115.2213", installs: 54},
+ "{e4f94d1e-2f53-401e-8885-681602c0ddd8}": {minVersion: "1.0.1-signed.1-signed", installs: 54},
+ "{FBF6D7FB-F305-4445-BB3D-FEF66579A033}": {minVersion: "6", installs: 54},
+ "5aa55fd5-6e61-4896-b186-fdc6f298ec92@mozilla": {minVersion: "0.1.2.1-signed", installs: 54},
+ "fireml@sirma.bg": {minVersion: "1.1.11.1-signed.1-signed", installs: 54},
+ "info@priceblink.com": {minVersion: "4.8", installs: 54},
+ "jid0-f82gosWvE8oeGQt6WDCGRF1Dy7Q@jetpack": {minVersion: "1.0.003", installs: 54},
+ "jid0-hyjN250ZzTOOX3evFwwAQBxE4ik@jetpack": {minVersion: "6.0.1-signed.1-signed", installs: 54},
+ "restart@restart.org": {minVersion: "0.5.1-signed.1-signed", installs: 54},
+ "webmaster@keep-tube.com": {minVersion: "1.2.1-signed.1-signed", installs: 54},
+ "eliteproxyswitcher@my-proxy.com": {minVersion: "1.2.0.2.1-signed.1-signed", installs: 53},
+ "foxfilter@inspiredeffect.net": {minVersion: "7.7.1-signed.1-signed", installs: 53},
+ "searchprivacy@searchprivacy.co": {minVersion: "1.15", installs: 53},
+ "SignPlugin@pekao.pl": {minVersion: "1.4.0.73", installs: 53},
+ "{15fe27f3-e5ab-2d59-4c5c-dadc7945bdbd}": {minVersion: "2.1.1.1-signed.1-signed", installs: 52},
+ "ipfuck@p4ul.info": {minVersion: "1.2.1.1-signed.1-signed", installs: 52},
+ "jyboy.yy@gmail.com": {minVersion: "1.0.4.1-signed.1-signed", installs: 52},
+ "MySafeTabs-the-extension1@mozilla.com": {minVersion: "0.1.9", installs: 52},
+ "saiful.neo@gmail.com": {minVersion: "3.0.1-signed.1-signed", installs: 52},
+ "sendtokindle@amazon.com": {minVersion: "1.0.2.76", installs: 52},
+ "smile1Button@amazon.com": {minVersion: "1.0.1-signed.1-signed", installs: 52},
+ "whodeletedme@deleted.io": {minVersion: "0.3.3", installs: 52},
+ "{C0CB8BA3-6C1B-47e8-A6AB-1FAB889562D9}": {minVersion: "0.7.6", installs: 51},
+ "@irctctatkal": {minVersion: "2.0.0", installs: 51},
+ "antgroup@antdownloadmanager.com": {minVersion: "0.1.7", installs: 51},
+ "downloadplan@firefoxmania.uci.cu": {minVersion: "1.3.1-signed.1-signed", installs: 51},
+ "jid1-AoXeeOB4j7kFdA@jetpack": {minVersion: "8.1", installs: 51},
+ "memoryrestart@teamextension.com": {minVersion: "1.18.1-signed.1-signed", installs: 51},
+ "multifox-toolbar-button@rbaldwin": {minVersion: "4.28.1-signed.1-signed", installs: 51},
+ "soaclient@santoso": {minVersion: "0.2.1-signed.1-signed", installs: 51},
+ "speeddns@gmail.com": {minVersion: "0.2.1-signed.1-signed", installs: 51},
+ "windowandtablimiter@weintraut.net": {minVersion: "4.28.1-signed.1-signed", installs: 51},
+ "@Recipes": {minVersion: "0.2.0", installs: 50},
+ "{e6a9a96e-4a08-4719-b9bd-0e91c35aaabc}": {minVersion: "1.3.1.1-signed.1-signed", installs: 50},
+ "autopager@mozilla.org": {minVersion: "0.8.0.10.1-signed.1-signed", installs: 50},
+ "btpersonas@brandthunder.com": {minVersion: "2.0.4.7", installs: 50},
+ "gdrivepanel@alejandrobrizuela.com.ar": {minVersion: "1.0.2.1-signed.1-signed", installs: 50},
+ "jid1-m3kqTBs1zKXXaA@jetpack": {minVersion: "0.2.6.1-signed.1-signed", installs: 50},
+ "qqmail_plugin_for_firefox@tencent.com": {minVersion: "1.0.0.22", installs: 50},
+ "savefileto@mozdev.org": {minVersion: "2.5.5", installs: 50},
+ "seodoctor@prelovac.com": {minVersion: "1.6.5.1-signed.1-signed", installs: 50},
+ "support@todoist.com": {minVersion: "4.0.5", installs: 50},
+ "toolbar_TeoMediaTB@apn.ask.com": {minVersion: "135.3", installs: 50},
+ "txftn@tencent.com": {minVersion: "1.0.0.7", installs: 50},
+};
+
+// ================== END OF LIST FOR 51 ======================
+
+// We use these named policies to correlate the telemetry
+// data with them, in order to understand how each set
+// is behaving in the wild.
+const RolloutPolicy = {
+ // Used during 48 Beta cycle
+ "2a": { addons: set2, webextensions: true },
+ "2b": { addons: set2, webextensions: false },
+
+ // Set agreed for Release 49
+ "49a": { addons: set49Release, webextensions: true },
+ "49b": { addons: set49Release, webextensions: false },
+
+ // Smaller set that can be used for Release 49
+ "49limiteda": { addons: set49PaneOnly, webextensions: true },
+ "49limitedb": { addons: set49PaneOnly, webextensions: false },
+
+ // Beta testing on 50
+ "50allmpc": { addons: [], webextensions: true, mpc: true },
+
+ // Beta testing on 51
+ "51alladdons": { addons: [], webextensions: true, alladdons: true },
+
+ // 51 release
+ "51set1": { addonsv2: set51Release, installs: 50, webextensions: true, mpc: true },
+ "51set2": { addonsv2: set51Release, installs: 100, webextensions: true, mpc: true },
+ "51set3": { addonsv2: set51Release, installs: 300, webextensions: true, mpc: true },
+ "51set4": { addonsv2: set51Release, installs: 1000, webextensions: true, mpc: true },
+
+ // ESR
+ "esrA": { addons: [], mpc: true, webextensions: true },
+ "esrB": { addons: [], mpc: true, webextensions: false },
+ "esrC": { addons: [], mpc: false, webextensions: true },
+
+ "xpcshell-test": { addons: [ADDONS.test1, ADDONS.test2], webextensions: false },
+};
+
+Object.defineProperty(this, "isAddonPartOfE10SRollout", {
+ configurable: false,
+ enumerable: false,
+ writable: false,
+ value: function isAddonPartOfE10SRollout(aAddon) {
+ let blocklist = Preferences.get(PREF_E10S_ADDON_BLOCKLIST, "");
+ let policyId = Preferences.get(PREF_E10S_ADDON_POLICY, "");
+
+ if (!policyId || !RolloutPolicy.hasOwnProperty(policyId)) {
+ return false;
+ }
+
+ if (blocklist && blocklist.indexOf(aAddon.id) > -1) {
+ return false;
+ }
+
+ let policy = RolloutPolicy[policyId];
+
+ if (aAddon.mpcOptedOut == true) {
+ return false;
+ }
+
+ if (policy.alladdons) {
+ return true;
+ }
+
+ if (policy.webextensions && aAddon.type == "webextension") {
+ return true;
+ }
+
+ if (policy.mpc && aAddon.multiprocessCompatible) {
+ return true;
+ }
+
+ if (policy.addonsv2) {
+ if (aAddon.id in policy.addonsv2) {
+ let rolloutAddon = policy.addonsv2[aAddon.id];
+
+ if (rolloutAddon.installs >= policy.installs &&
+ Services.vc.compare(aAddon.version, rolloutAddon.minVersion) >= 0) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ for (let rolloutAddon of policy.addons) {
+ if (aAddon.id == rolloutAddon.id &&
+ Services.vc.compare(aAddon.version, rolloutAddon.minVersion) >= 0) {
+ return true;
+ }
+ }
+
+ return false;
+ },
+});
diff --git a/toolkit/mozapps/extensions/internal/GMPProvider.jsm b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
new file mode 100644
index 000000000..9bb34a7af
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/GMPProvider.jsm
@@ -0,0 +1,699 @@
+/* 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/extensions/internal/LightweightThemeImageOptimizer.jsm b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
new file mode 100644
index 000000000..49dfa237f
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/LightweightThemeImageOptimizer.jsm
@@ -0,0 +1,180 @@
+/* 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/extensions/internal/PluginProvider.jsm b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
new file mode 100644
index 000000000..075159a9a
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/PluginProvider.jsm
@@ -0,0 +1,600 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+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/extensions/internal/ProductAddonChecker.jsm b/toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm
new file mode 100644
index 000000000..f98dd2a94
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/ProductAddonChecker.jsm
@@ -0,0 +1,467 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+const LOCAL_EME_SOURCES = [{
+ "id": "gmp-eme-adobe",
+ "src": "chrome://global/content/gmp-sources/eme-adobe.json"
+}, {
+ "id": "gmp-gmpopenh264",
+ "src": "chrome://global/content/gmp-sources/openh264.json"
+}, {
+ "id": "gmp-widevinecdm",
+ "src": "chrome://global/content/gmp-sources/widevinecdm.json"
+}];
+
+this.EXPORTED_SYMBOLS = [ "ProductAddonChecker" ];
+
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/Log.jsm");
+Cu.import("resource://gre/modules/CertUtils.jsm");
+/* globals checkCert, BadCertHandler*/
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+
+/* globals GMPPrefs */
+XPCOMUtils.defineLazyModuleGetter(this, "GMPPrefs",
+ "resource://gre/modules/GMPUtils.jsm");
+
+/* globals OS */
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+
+// This exists so that tests can override the XHR behaviour for downloading
+// the addon update XML file.
+var CreateXHR = function() {
+ return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
+ createInstance(Ci.nsISupports);
+}
+
+var logger = Log.repository.getLogger("addons.productaddons");
+
+/**
+ * Number of milliseconds after which we need to cancel `downloadXML`.
+ *
+ * Bug 1087674 suggests that the XHR we use in `downloadXML` 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 TIMEOUT_DELAY_MS = 20000;
+// Chunk size for the incremental downloader
+const DOWNLOAD_CHUNK_BYTES_SIZE = 300000;
+// Incremental downloader interval
+const DOWNLOAD_INTERVAL = 0;
+// How much of a file to read into memory at a time for hashing
+const HASH_CHUNK_SIZE = 8192;
+
+/**
+ * Gets the status of an XMLHttpRequest either directly or from its underlying
+ * channel.
+ *
+ * @param request
+ * The XMLHttpRequest.
+ * @return an integer status value.
+ */
+function getRequestStatus(request) {
+ let status = null;
+ try {
+ status = request.status;
+ }
+ catch (e) {
+ }
+
+ if (status != null) {
+ return status;
+ }
+
+ return request.channel.QueryInterface(Ci.nsIRequest).status;
+}
+
+/**
+ * Downloads an XML document from a URL optionally testing the SSL certificate
+ * for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to the DOM document downloaded or rejects
+ * with a JS exception in case of error.
+ */
+function downloadXML(url, allowNonBuiltIn = false, allowedCerts = null) {
+ return new Promise((resolve, reject) => {
+ let request = CreateXHR();
+ // This is here to let unit test code override XHR
+ if (request.wrappedJSObject) {
+ request = request.wrappedJSObject;
+ }
+ request.open("GET", url, true);
+ request.channel.notificationCallbacks = new BadCertHandler(allowNonBuiltIn);
+ // Prevent the request from reading from the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.LOAD_BYPASS_CACHE;
+ // Prevent the request from writing to the cache.
+ request.channel.loadFlags |= Ci.nsIRequest.INHIBIT_CACHING;
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (request.channel instanceof Ci.nsIHttpChannelInternal) {
+ request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ request.timeout = TIMEOUT_DELAY_MS;
+
+ 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.
+ request.setRequestHeader("Cache-Control", "no-cache");
+ // HTTP/1.0 servers might not implement Cache-Control and
+ // might only implement Pragma: no-cache
+ request.setRequestHeader("Pragma", "no-cache");
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading XML, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+
+ let success = (event) => {
+ logger.info("Completed downloading document");
+ let request = event.target;
+
+ try {
+ checkCert(request.channel, allowNonBuiltIn, allowedCerts);
+ } catch (ex) {
+ logger.error("Request failed certificate checks: " + ex);
+ ex.status = getRequestStatus(request);
+ reject(ex);
+ return;
+ }
+
+ resolve(request.responseXML);
+ };
+
+ request.addEventListener("error", fail, false);
+ request.addEventListener("abort", fail, false);
+ request.addEventListener("timeout", fail, false);
+ request.addEventListener("load", success, false);
+
+ logger.info("sending request to: " + url);
+ request.send(null);
+ });
+}
+
+function downloadJSON(uri) {
+ logger.info("fetching config from: " + uri);
+ return new Promise((resolve, reject) => {
+ let xmlHttp = new ServiceRequest({mozAnon: true});
+
+ xmlHttp.onload = function(aResponse) {
+ resolve(JSON.parse(this.responseText));
+ };
+
+ xmlHttp.onerror = function(e) {
+ reject("Fetching " + uri + " results in error code: " + e.target.status);
+ };
+
+ xmlHttp.open("GET", uri);
+ xmlHttp.overrideMimeType("application/json");
+ xmlHttp.send();
+ });
+}
+
+
+/**
+ * Parses a list of add-ons from a DOM document.
+ *
+ * @param document
+ * The DOM document to parse.
+ * @return null if there is no <addons> element otherwise an object containing
+ * an array of the addons listed and a field notifying whether the
+ * fallback was used.
+ */
+function parseXML(document) {
+ // Check that the root element is correct
+ if (document.documentElement.localName != "updates") {
+ throw new Error("got node name: " + document.documentElement.localName +
+ ", expected: updates");
+ }
+
+ // Check if there are any addons elements in the updates element
+ let addons = document.querySelector("updates:root > addons");
+ if (!addons) {
+ return null;
+ }
+
+ let results = [];
+ let addonList = document.querySelectorAll("updates:root > addons > addon");
+ for (let addonElement of addonList) {
+ let addon = {};
+
+ for (let name of ["id", "URL", "hashFunction", "hashValue", "version", "size"]) {
+ if (addonElement.hasAttribute(name)) {
+ addon[name] = addonElement.getAttribute(name);
+ }
+ }
+ addon.size = Number(addon.size) || undefined;
+
+ results.push(addon);
+ }
+
+ return {
+ usedFallback: false,
+ gmpAddons: results
+ };
+}
+
+/**
+ * If downloading from the network fails (AUS server is down),
+ * load the sources from local build configuration.
+ */
+function downloadLocalConfig() {
+
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return Promise.all(LOCAL_EME_SOURCES.map(conf => {
+ return downloadJSON(conf.src).then(addons => {
+
+ let platforms = addons.vendors[conf.id].platforms;
+ let target = Services.appinfo.OS + "_" + UpdateUtils.ABI;
+ let details = null;
+
+ while (!details) {
+ if (!(target in platforms)) {
+ // There was no matching platform so return false, this addon
+ // will be filtered from the results below
+ logger.info("no details found for: " + target);
+ return false;
+ }
+ // Field either has the details of the binary or is an alias
+ // to another build target key that does
+ if (platforms[target].alias) {
+ target = platforms[target].alias;
+ } else {
+ details = platforms[target];
+ }
+ }
+
+ logger.info("found plugin: " + conf.id);
+ return {
+ "id": conf.id,
+ "URL": details.fileUrl,
+ "hashFunction": addons.hashFunction,
+ "hashValue": details.hashValue,
+ "version": addons.vendors[conf.id].version,
+ "size": details.filesize
+ };
+ });
+ })).then(addons => {
+
+ // Some filters may not match this platform so
+ // filter those out
+ addons = addons.filter(x => x !== false);
+
+ return {
+ usedFallback: true,
+ gmpAddons: addons
+ };
+ });
+}
+
+/**
+ * Downloads file from a URL using XHR.
+ *
+ * @param url
+ * The url to download from.
+ * @return a promise that resolves to the path of a temporary file or rejects
+ * with a JS exception in case of error.
+ */
+function downloadFile(url) {
+ return new Promise((resolve, reject) => {
+ let xhr = new XMLHttpRequest();
+ xhr.onload = function(response) {
+ logger.info("downloadXHR File download. status=" + xhr.status);
+ if (xhr.status != 200 && xhr.status != 206) {
+ reject(Components.Exception("File download failed", xhr.status));
+ return;
+ }
+ Task.spawn(function* () {
+ let f = yield OS.File.openUnique(OS.Path.join(OS.Constants.Path.tmpDir, "tmpaddon"));
+ let path = f.path;
+ logger.info(`Downloaded file will be saved to ${path}`);
+ yield f.file.close();
+ yield OS.File.writeAtomic(path, new Uint8Array(xhr.response));
+ return path;
+ }).then(resolve, reject);
+ };
+
+ let fail = (event) => {
+ let request = event.target;
+ let status = getRequestStatus(request);
+ let message = "Failed downloading via XHR, status: " + status + ", reason: " + event.type;
+ logger.warn(message);
+ let ex = new Error(message);
+ ex.status = status;
+ reject(ex);
+ };
+ xhr.addEventListener("error", fail);
+ xhr.addEventListener("abort", fail);
+
+ xhr.responseType = "arraybuffer";
+ try {
+ xhr.open("GET", url);
+ // Use conservative TLS settings. See bug 1325501.
+ // TODO move to ServiceRequest.
+ if (xhr.channel instanceof Ci.nsIHttpChannelInternal) {
+ xhr.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+ }
+ xhr.send(null);
+ } catch (ex) {
+ reject(ex);
+ }
+ });
+}
+
+/**
+ * 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;
+}
+
+/**
+ * Calculates the hash of a file.
+ *
+ * @param hashFunction
+ * The type of hash function to use, must be supported by nsICryptoHash.
+ * @param path
+ * The path of the file to hash.
+ * @return a promise that resolves to hash of the file or rejects with a JS
+ * exception in case of error.
+ */
+var computeHash = Task.async(function*(hashFunction, path) {
+ let file = yield OS.File.open(path, { existing: true, read: true });
+ try {
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.initWithString(hashFunction);
+
+ let bytes;
+ do {
+ bytes = yield file.read(HASH_CHUNK_SIZE);
+ hasher.update(bytes, bytes.length);
+ } while (bytes.length == HASH_CHUNK_SIZE);
+
+ return binaryToHex(hasher.finish(false));
+ }
+ finally {
+ yield file.close();
+ }
+});
+
+/**
+ * Verifies that a downloaded file matches what was expected.
+ *
+ * @param properties
+ * The properties to check, `size` and `hashFunction` with `hashValue`
+ * are supported. Any properties missing won't be checked.
+ * @param path
+ * The path of the file to check.
+ * @return a promise that resolves if the file matched or rejects with a JS
+ * exception in case of error.
+ */
+var verifyFile = Task.async(function*(properties, path) {
+ if (properties.size !== undefined) {
+ let stat = yield OS.File.stat(path);
+ if (stat.size != properties.size) {
+ throw new Error("Downloaded file was " + stat.size + " bytes but expected " + properties.size + " bytes.");
+ }
+ }
+
+ if (properties.hashFunction !== undefined) {
+ let expectedDigest = properties.hashValue.toLowerCase();
+ let digest = yield computeHash(properties.hashFunction, path);
+ if (digest != expectedDigest) {
+ throw new Error("Hash was `" + digest + "` but expected `" + expectedDigest + "`.");
+ }
+ }
+});
+
+const ProductAddonChecker = {
+ /**
+ * Downloads a list of add-ons from a URL optionally testing the SSL
+ * certificate for certain attributes.
+ *
+ * @param url
+ * The url to download from.
+ * @param allowNonBuiltIn
+ * Whether to trust SSL certificates without a built-in CA issuer.
+ * @param allowedCerts
+ * The list of certificate attributes to match the SSL certificate
+ * against or null to skip checks.
+ * @return a promise that resolves to an object containing the list of add-ons
+ * and whether the local fallback was used, or rejects with a JS
+ * exception in case of error.
+ */
+ getProductAddonList: function(url, allowNonBuiltIn = false, allowedCerts = null) {
+ if (!GMPPrefs.get(GMPPrefs.KEY_UPDATE_ENABLED, true)) {
+ logger.info("Updates are disabled via media.gmp-manager.updateEnabled");
+ return Promise.resolve({usedFallback: true, gmpAddons: []});
+ }
+
+ return downloadXML(url, allowNonBuiltIn, allowedCerts)
+ .then(parseXML)
+ .catch(downloadLocalConfig);
+ },
+
+ /**
+ * Downloads an add-on to a local file and checks that it matches the expected
+ * file. The caller is responsible for deleting the temporary file returned.
+ *
+ * @param addon
+ * The addon to download.
+ * @return a promise that resolves to the temporary file downloaded or rejects
+ * with a JS exception in case of error.
+ */
+ downloadAddon: Task.async(function*(addon) {
+ let path = yield downloadFile(addon.URL);
+ try {
+ yield verifyFile(addon, path);
+ return path;
+ }
+ catch (e) {
+ yield OS.File.remove(path);
+ throw e;
+ }
+ })
+}
diff --git a/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js b/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js
new file mode 100644
index 000000000..f4f557fc2
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/SpellCheckDictionaryBootstrap.js
@@ -0,0 +1,17 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var hunspell, dir;
+
+function startup(data) {
+ hunspell = Components.classes["@mozilla.org/spellchecker/engine;1"]
+ .getService(Components.interfaces.mozISpellCheckingEngine);
+ dir = data.installPath.clone();
+ dir.append("dictionaries");
+ hunspell.addDirectory(dir);
+}
+
+function shutdown() {
+ hunspell.removeDirectory(dir);
+}
diff --git a/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js b/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
new file mode 100644
index 000000000..a920c2eae
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/WebExtensionBootstrap.js
@@ -0,0 +1,39 @@
+/* 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/extensions/internal/XPIProvider.jsm b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
new file mode 100644
index 000000000..87e09cef1
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -0,0 +1,9305 @@
+ /* 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, "isAddonPartOfE10SRollout",
+ "resource://gre/modules/addons/E10SAddonsRollout.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_E10S_BLOCK_ENABLE = "extensions.e10sBlocksEnabling";
+const PREF_E10S_ADDON_BLOCKLIST = "extensions.e10s.rollout.blocklist";
+const PREF_E10S_ADDON_POLICY = "extensions.e10s.rollout.policy";
+const PREF_E10S_HAS_NONEXEMPT_ADDON = "extensions.e10s.rollout.hasAddon";
+
+const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
+const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
+
+const PREF_CHECKCOMAT_THEMEOVERRIDE = "extensions.checkCompatibility.temporaryThemeOverride_minAppVersion";
+
+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 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 {
+ let app = aAddon.matchingTargetApplication;
+ if (!app) {
+ logger.warn(`Add-on ${aAddon.id} is not compatible with target application.`);
+ return false;
+ }
+
+ // XXX Temporary solution to let applications opt-in to make themes safer
+ // following significant UI changes even if checkCompatibility=false has
+ // been set, until we get bug 962001.
+ if (aAddon.type == "theme" && app.id == Services.appinfo.ID) {
+ try {
+ let minCompatVersion = Services.prefs.getCharPref(PREF_CHECKCOMAT_THEMEOVERRIDE);
+ if (minCompatVersion &&
+ Services.vc.compare(minCompatVersion, app.maxVersion) > 0) {
+ logger.warn(`Theme ${aAddon.id} is not compatible with application version.`);
+ return false;
+ }
+ } catch (e) {}
+ }
+ }
+
+ 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: TOOLKIT_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);
+ Services.prefs.addObserver(PREF_E10S_ADDON_BLOCKLIST, this, false);
+ Services.prefs.addObserver(PREF_E10S_ADDON_POLICY, 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;
+
+ case PREF_E10S_ADDON_BLOCKLIST:
+ case PREF_E10S_ADDON_POLICY:
+ XPIDatabase.updateAddonsBlockingE10s();
+ break;
+ }
+ }
+ },
+
+ /**
+ * Determine if an add-on should be blocking e10s if enabled.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if enabling the add-on should block e10s
+ */
+ isBlockingE10s: function(aAddon) {
+ if (aAddon.type != "extension" &&
+ aAddon.type != "webextension" &&
+ aAddon.type != "theme")
+ return false;
+
+ // The hotfix is exempt
+ let hotfixID = Preferences.get(PREF_EM_HOTFIX_ID, undefined);
+ if (hotfixID && hotfixID == aAddon.id)
+ return false;
+
+ // The default theme is exempt
+ if (aAddon.type == "theme" &&
+ aAddon.internalName == XPIProvider.defaultSkin)
+ return false;
+
+ // System add-ons are exempt
+ let locName = aAddon._installLocation ? aAddon._installLocation.name
+ : undefined;
+ if (locName == KEY_APP_SYSTEM_DEFAULTS ||
+ locName == KEY_APP_SYSTEM_ADDONS)
+ return false;
+
+ if (isAddonPartOfE10SRollout(aAddon)) {
+ Preferences.set(PREF_E10S_HAS_NONEXEMPT_ADDON, true);
+ return false;
+ }
+
+ logger.debug("Add-on " + aAddon.id + " blocks e10s rollout.");
+ return true;
+ },
+
+ /**
+ * In some cases having add-ons active blocks e10s but turning off e10s
+ * requires a restart so some add-ons that are normally restartless will
+ * require a restart to install or enable.
+ *
+ * @param aAddon
+ * The add-on to test
+ * @return true if enabling the add-on requires a restart
+ */
+ e10sBlocksEnabling: function(aAddon) {
+ // If the preference isn't set then don't block anything
+ if (!Preferences.get(PREF_E10S_BLOCK_ENABLE, false))
+ return false;
+
+ // If e10s isn't active then don't block anything
+ if (!Services.appinfo.browserTabsRemoteAutostart)
+ return false;
+
+ return this.isBlockingE10s(aAddon);
+ },
+
+ /**
+ * 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;
+ }
+
+ if (this.e10sBlocksEnabling(aAddon))
+ return true;
+
+ 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;
+
+ if (this.e10sBlocksEnabling(aAddon))
+ return true;
+
+ // 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
+
+ // 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)
+ 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)
+ 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/extensions/internal/XPIProviderUtils.js b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
new file mode 100644
index 000000000..d04eb207c
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/XPIProviderUtils.js
@@ -0,0 +1,2255 @@
+/* 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);
+ }
+
+ this.updateAddonsBlockingE10s();
+ 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();
+ },
+
+ updateAddonsBlockingE10s: function() {
+ let blockE10s = false;
+
+ Preferences.set(PREF_E10S_HAS_NONEXEMPT_ADDON, false);
+ for (let [, addon] of this.addonDB) {
+ let active = (addon.visible && !addon.disabled && !addon.pendingUninstall);
+
+ if (active && XPIProvider.isBlockingE10s(addon)) {
+ blockE10s = true;
+ break;
+ }
+ }
+ Preferences.set(PREF_E10S_BLOCKED_BY_ADDONS, blockE10s);
+ },
+
+ /**
+ * 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/extensions/internal/moz.build b/toolkit/mozapps/extensions/internal/moz.build
new file mode 100644
index 000000000..28c34f8c9
--- /dev/null
+++ b/toolkit/mozapps/extensions/internal/moz.build
@@ -0,0 +1,36 @@
+# -*- 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 += [
+ 'AddonLogging.jsm',
+ 'AddonRepository.jsm',
+ 'AddonRepository_SQLiteMigrator.jsm',
+ 'AddonUpdateChecker.jsm',
+ 'APIExtensionBootstrap.js',
+ 'Content.js',
+ 'E10SAddonsRollout.jsm',
+ 'GMPProvider.jsm',
+ 'LightweightThemeImageOptimizer.jsm',
+ 'ProductAddonChecker.jsm',
+ 'SpellCheckDictionaryBootstrap.js',
+ 'WebExtensionBootstrap.js',
+ 'XPIProvider.jsm',
+ 'XPIProviderUtils.js',
+]
+
+TESTING_JS_MODULES += [
+ 'AddonTestUtils.jsm',
+]
+
+# Don't ship unused providers on Android
+if CONFIG['MOZ_WIDGET_TOOLKIT'] != 'android':
+ EXTRA_JS_MODULES.addons += [
+ 'PluginProvider.jsm',
+ ]
+
+EXTRA_PP_JS_MODULES.addons += [
+ 'AddonConstants.jsm',
+]
diff --git a/toolkit/mozapps/extensions/jar.mn b/toolkit/mozapps/extensions/jar.mn
new file mode 100644
index 000000000..ecf64b7ef
--- /dev/null
+++ b/toolkit/mozapps/extensions/jar.mn
@@ -0,0 +1,35 @@
+# 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 (content/updateinfo.xsl)
+ content/mozapps/extensions/about.xul (content/about.xul)
+ content/mozapps/extensions/about.js (content/about.js)
+ content/mozapps/extensions/list.xul (content/list.xul)
+ content/mozapps/extensions/list.js (content/list.js)
+ content/mozapps/extensions/blocklist.xul (content/blocklist.xul)
+ content/mozapps/extensions/blocklist.js (content/blocklist.js)
+ content/mozapps/extensions/blocklist.css (content/blocklist.css)
+ content/mozapps/extensions/blocklist.xml (content/blocklist.xml)
+* content/mozapps/extensions/update.xul (content/update.xul)
+ content/mozapps/extensions/update.js (content/update.js)
+ 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/pluginPrefs.xul (content/pluginPrefs.xul)
+ content/mozapps/extensions/gmpPrefs.xul (content/gmpPrefs.xul)
+ content/mozapps/extensions/OpenH264-license.txt (content/OpenH264-license.txt)
+#endif
+ content/mozapps/extensions/setting.xml (content/setting.xml)
+ content/mozapps/xpinstall/xpinstallConfirm.xul (content/xpinstallConfirm.xul)
+ content/mozapps/xpinstall/xpinstallConfirm.js (content/xpinstallConfirm.js)
+ content/mozapps/xpinstall/xpinstallConfirm.css (content/xpinstallConfirm.css)
+ content/mozapps/xpinstall/xpinstallItem.xml (content/xpinstallItem.xml)
diff --git a/toolkit/mozapps/extensions/moz.build b/toolkit/mozapps/extensions/moz.build
new file mode 100644
index 000000000..12640e115
--- /dev/null
+++ b/toolkit/mozapps/extensions/moz.build
@@ -0,0 +1,66 @@
+# -*- 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/.
+
+SPHINX_TREES['addon-manager'] = 'docs'
+
+if CONFIG['MOZ_BUILD_APP'] == 'mobile/android':
+ DEFINES['MOZ_FENNEC'] = True
+
+DIRS += ['internal']
+TEST_DIRS += ['test']
+
+XPIDL_SOURCES += [
+ 'amIAddonManager.idl',
+ 'amIAddonPathService.idl',
+ 'amIWebInstaller.idl',
+ 'amIWebInstallListener.idl',
+]
+
+XPIDL_MODULE = 'extensions'
+
+EXTRA_COMPONENTS += [
+ 'addonManager.js',
+ 'amContentHandler.js',
+ 'amInstallTrigger.js',
+ 'amWebAPI.js',
+ 'amWebInstallListener.js',
+ 'nsBlocklistService.js',
+ 'nsBlocklistServiceContent.js',
+]
+
+EXTRA_PP_COMPONENTS += [
+ 'extensions.manifest',
+]
+
+EXTRA_JS_MODULES += [
+ 'AddonManager.jsm',
+ 'ChromeManifestParser.jsm',
+ 'DeferredSave.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'
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Add-ons Manager')
diff --git a/toolkit/mozapps/extensions/nsBlocklistService.js b/toolkit/mozapps/extensions/nsBlocklistService.js
new file mode 100644
index 000000000..a7b49a99c
--- /dev/null
+++ b/toolkit/mozapps/extensions/nsBlocklistService.js
@@ -0,0 +1,1678 @@
+/* -*- 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";
+
+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");
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+try {
+ // AddonManager.jsm doesn't allow itself to be imported in the child
+ // process. We're used in the child process (for now), so guard against
+ // this.
+ Components.utils.import("resource://gre/modules/AddonManager.jsm");
+ /* globals AddonManagerPrivate*/
+} catch (e) {
+}
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "OS",
+ "resource://gre/modules/osfile.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ServiceRequest",
+ "resource://gre/modules/ServiceRequest.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "Task",
+ "resource://gre/modules/Task.jsm");
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_BLOCKLIST = "blocklist.xml";
+const PREF_BLOCKLIST_LASTUPDATETIME = "app.update.lastUpdateTime.blocklist-background-update-timer";
+const PREF_BLOCKLIST_URL = "extensions.blocklist.url";
+const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_BLOCKLIST_INTERVAL = "extensions.blocklist.interval";
+const PREF_BLOCKLIST_LEVEL = "extensions.blocklist.level";
+const PREF_BLOCKLIST_PINGCOUNTTOTAL = "extensions.blocklist.pingCountTotal";
+const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
+const PREF_BLOCKLIST_SUPPRESSUI = "extensions.blocklist.suppressUI";
+const PREF_ONECRL_VIA_AMO = "security.onecrl.via.amo";
+const PREF_BLOCKLIST_UPDATE_ENABLED = "services.blocklist.update_enabled";
+const PREF_GENERAL_USERAGENT_LOCALE = "general.useragent.locale";
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_EM_LOGGING_ENABLED = "extensions.logging.enabled";
+const XMLURI_BLOCKLIST = "http://www.mozilla.org/2006/addons-blocklist";
+const XMLURI_PARSE_ERROR = "http://www.mozilla.org/newlayout/xml/parsererror.xml"
+const UNKNOWN_XPCOM_ABI = "unknownABI";
+const URI_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul"
+const DEFAULT_SEVERITY = 3;
+const DEFAULT_LEVEL = 2;
+const MAX_BLOCK_LEVEL = 3;
+const SEVERITY_OUTDATED = 0;
+const VULNERABILITYSTATUS_NONE = 0;
+const VULNERABILITYSTATUS_UPDATE_AVAILABLE = 1;
+const VULNERABILITYSTATUS_NO_UPDATE = 2;
+
+const EXTENSION_BLOCK_FILTERS = ["id", "name", "creator", "homepageURL", "updateURL"];
+
+var gLoggingEnabled = null;
+var gBlocklistEnabled = true;
+var gBlocklistLevel = DEFAULT_LEVEL;
+
+XPCOMUtils.defineLazyServiceGetter(this, "gConsole",
+ "@mozilla.org/consoleservice;1",
+ "nsIConsoleService");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gVersionChecker",
+ "@mozilla.org/xpcom/version-comparator;1",
+ "nsIVersionComparator");
+
+XPCOMUtils.defineLazyServiceGetter(this, "gCertBlocklistService",
+ "@mozilla.org/security/certblocklist;1",
+ "nsICertBlocklist");
+
+XPCOMUtils.defineLazyGetter(this, "gPref", function() {
+ return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch);
+});
+
+// From appinfo in Services.jsm. It is not possible to use the one in
+// Services.jsm since it will not successfully QueryInterface nsIXULAppInfo in
+// xpcshell tests due to other code calling Services.appinfo before the
+// nsIXULAppInfo is created by the tests.
+XPCOMUtils.defineLazyGetter(this, "gApp", function() {
+ let appinfo = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ try {
+ appinfo.QueryInterface(Ci.nsIXULAppInfo);
+ } catch (ex) {
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!(ex instanceof Components.Exception) ||
+ ex.result != Cr.NS_NOINTERFACE)
+ throw ex;
+ }
+ return appinfo;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gABI", function() {
+ let abi = null;
+ try {
+ abi = gApp.XPCOMABI;
+ }
+ catch (e) {
+ LOG("BlockList Global gABI: XPCOM ABI unknown.");
+ }
+
+ if (AppConstants.platform == "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;
+ }
+ return abi;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gOSVersion", function() {
+ let osVersion;
+ let sysInfo = Cc["@mozilla.org/system-info;1"].
+ getService(Ci.nsIPropertyBag2);
+ try {
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+ }
+ catch (e) {
+ LOG("BlockList Global gOSVersion: OS Version unknown.");
+ }
+
+ if (osVersion) {
+ 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;
+});
+
+// shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function() {
+ let temp = { };
+ Components.utils.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console..
+ */
+function LOG(string) {
+ if (gLoggingEnabled) {
+ dump("*** " + string + "\n");
+ gConsole.logStringMessage(string);
+ }
+}
+
+/**
+ * Gets a preference value, handling the case where there is no default.
+ * @param func
+ * The name of the preference function to call, on nsIPrefBranch
+ * @param preference
+ * The name of the preference
+ * @param defaultValue
+ * The default value to return in the event the preference has
+ * no setting
+ * @returns The value of the preference, or undefined if there was no
+ * user or default value.
+ */
+function getPref(func, preference, defaultValue) {
+ try {
+ return gPref[func](preference);
+ }
+ catch (e) {
+ }
+ return defaultValue;
+}
+
+/**
+ * Constructs a URI to a spec.
+ * @param spec
+ * The spec to construct a URI to
+ * @returns The nsIURI constructed.
+ */
+function newURI(spec) {
+ var ioServ = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ return ioServ.newURI(spec, null, null);
+}
+
+// Restarts the application checking in with observers first
+function restartApp() {
+ // Notify all windows that an application quit has been requested.
+ var os = Cc["@mozilla.org/observer-service;1"].
+ getService(Ci.nsIObserverService);
+ var cancelQuit = Cc["@mozilla.org/supports-PRBool;1"].
+ createInstance(Ci.nsISupportsPRBool);
+ os.notifyObservers(cancelQuit, "quit-application-requested", null);
+
+ // Something aborted the quit process.
+ if (cancelQuit.data)
+ return;
+
+ var as = Cc["@mozilla.org/toolkit/app-startup;1"].
+ getService(Ci.nsIAppStartup);
+ as.quit(Ci.nsIAppStartup.eRestart | Ci.nsIAppStartup.eAttemptQuit);
+}
+
+/**
+ * Checks whether this blocklist element is valid for the current OS and ABI.
+ * If the element has an "os" attribute then the current OS must appear in
+ * its comma separated list for the element to be valid. Similarly for the
+ * xpcomabi attribute.
+ */
+function matchesOSABI(blocklistElement) {
+ if (blocklistElement.hasAttribute("os")) {
+ var choices = blocklistElement.getAttribute("os").split(",");
+ if (choices.length > 0 && choices.indexOf(gApp.OS) < 0)
+ return false;
+ }
+
+ if (blocklistElement.hasAttribute("xpcomabi")) {
+ choices = blocklistElement.getAttribute("xpcomabi").split(",");
+ if (choices.length > 0 && choices.indexOf(gApp.XPCOMABI) < 0)
+ return false;
+ }
+
+ return true;
+}
+
+/**
+ * Gets the current value of the locale. It's possible for this preference to
+ * be localized, so we have to do a little extra work here. Similar code
+ * exists in nsHttpHandler.cpp when building the UA string.
+ */
+function getLocale() {
+ try {
+ // Get the default branch
+ var defaultPrefs = gPref.getDefaultBranch(null);
+ return defaultPrefs.getComplexValue(PREF_GENERAL_USERAGENT_LOCALE,
+ Ci.nsIPrefLocalizedString).data;
+ } catch (e) {}
+
+ return gPref.getCharPref(PREF_GENERAL_USERAGENT_LOCALE);
+}
+
+/* Get the distribution pref values, from defaults only */
+function getDistributionPrefValue(aPrefName) {
+ var prefValue = "default";
+
+ var defaults = gPref.getDefaultBranch(null);
+ try {
+ prefValue = defaults.getCharPref(aPrefName);
+ } catch (e) {
+ // use default when pref not found
+ }
+
+ return prefValue;
+}
+
+/**
+ * Parse a string representation of a regular expression. Needed because we
+ * use the /pattern/flags form (because it's detectable), which is only
+ * supported as a literal in JS.
+ *
+ * @param aStr
+ * String representation of regexp
+ * @return RegExp instance
+ */
+function parseRegExp(aStr) {
+ let lastSlash = aStr.lastIndexOf("/");
+ let pattern = aStr.slice(1, lastSlash);
+ let flags = aStr.slice(lastSlash + 1);
+ return new RegExp(pattern, flags);
+}
+
+/**
+ * Manages the Blocklist. The Blocklist is a representation of the contents of
+ * blocklist.xml and allows us to remotely disable / re-enable blocklisted
+ * items managed by the Extension Manager with an item's appDisabled property.
+ * It also blocklists plugins with data from blocklist.xml.
+ */
+
+function Blocklist() {
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ Services.obs.addObserver(this, "sessionstore-windows-restored", false);
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
+ gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL);
+ gPref.addObserver("extensions.blocklist.", this, false);
+ gPref.addObserver(PREF_EM_LOGGING_ENABLED, this, false);
+ this.wrappedJSObject = this;
+ // requests from child processes come in here, see receiveMessage.
+ Services.ppmm.addMessageListener("Blocklist:getPluginBlocklistState", this);
+ Services.ppmm.addMessageListener("Blocklist:content-blocklist-updated", this);
+}
+
+Blocklist.prototype = {
+ /**
+ * Extension ID -> array of Version Ranges
+ * Each value in the version range array is a JS Object that has the
+ * following properties:
+ * "minVersion" The minimum version in a version range (default = 0)
+ * "maxVersion" The maximum version in a version range (default = *)
+ * "targetApps" Application ID -> array of Version Ranges
+ * (default = current application ID)
+ * Each value in the version range array is a JS Object that
+ * has the following properties:
+ * "minVersion" The minimum version in a version range
+ * (default = 0)
+ * "maxVersion" The maximum version in a version range
+ * (default = *)
+ */
+ _addonEntries: null,
+ _gfxEntries: null,
+ _pluginEntries: null,
+
+ shutdown: function() {
+ Services.obs.removeObserver(this, "xpcom-shutdown");
+ Services.ppmm.removeMessageListener("Blocklist:getPluginBlocklistState", this);
+ Services.ppmm.removeMessageListener("Blocklist:content-blocklist-updated", this);
+ gPref.removeObserver("extensions.blocklist.", this);
+ gPref.removeObserver(PREF_EM_LOGGING_ENABLED, this);
+ },
+
+ observe: function(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "xpcom-shutdown":
+ this.shutdown();
+ break;
+ case "nsPref:changed":
+ switch (aData) {
+ case PREF_EM_LOGGING_ENABLED:
+ gLoggingEnabled = getPref("getBoolPref", PREF_EM_LOGGING_ENABLED, false);
+ break;
+ case PREF_BLOCKLIST_ENABLED:
+ gBlocklistEnabled = getPref("getBoolPref", PREF_BLOCKLIST_ENABLED, true);
+ this._loadBlocklist();
+ this._blocklistUpdated(null, null);
+ break;
+ case PREF_BLOCKLIST_LEVEL:
+ gBlocklistLevel = Math.min(getPref("getIntPref", PREF_BLOCKLIST_LEVEL, DEFAULT_LEVEL),
+ MAX_BLOCK_LEVEL);
+ this._blocklistUpdated(null, null);
+ break;
+ }
+ break;
+ case "sessionstore-windows-restored":
+ Services.obs.removeObserver(this, "sessionstore-windows-restored");
+ this._preloadBlocklist();
+ break;
+ }
+ },
+
+ // Message manager message handlers
+ receiveMessage: function(aMsg) {
+ switch (aMsg.name) {
+ case "Blocklist:getPluginBlocklistState":
+ return this.getPluginBlocklistState(aMsg.data.addonData,
+ aMsg.data.appVersion,
+ aMsg.data.toolkitVersion);
+ case "Blocklist:content-blocklist-updated":
+ Services.obs.notifyObservers(null, "content-blocklist-updated", null);
+ break;
+ default:
+ throw new Error("Unknown blocklist message received from content: " + aMsg.name);
+ }
+ return undefined;
+ },
+
+ /* See nsIBlocklistService */
+ isAddonBlocklisted: function(addon, appVersion, toolkitVersion) {
+ return this.getAddonBlocklistState(addon, appVersion, toolkitVersion) ==
+ Ci.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ /* See nsIBlocklistService */
+ getAddonBlocklistState: function(addon, appVersion, toolkitVersion) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+ return this._getAddonBlocklistState(addon, this._addonEntries,
+ appVersion, toolkitVersion);
+ },
+
+ /**
+ * Private version of getAddonBlocklistState that allows the caller to pass in
+ * the add-on blocklist entries to compare against.
+ *
+ * @param id
+ * The ID of the item to get the blocklist state for.
+ * @param version
+ * The version of the item to get the blocklist state for.
+ * @param addonEntries
+ * The add-on blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns The blocklist state for the item, one of the STATE constants as
+ * defined in nsIBlocklistService.
+ */
+ _getAddonBlocklistState: function(addon, addonEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ var blItem = this._findMatchingAddonEntry(addonEntries, addon);
+ if (!blItem)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ for (let currentblItem of blItem.versions) {
+ if (currentblItem.includesItem(addon.version, appVersion, toolkitVersion))
+ return currentblItem.severity >= gBlocklistLevel ? Ci.nsIBlocklistService.STATE_BLOCKED :
+ Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ }
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ /**
+ * Returns the set of prefs of the add-on stored in the blocklist file
+ * (probably to revert them on disabling).
+ * @param addon
+ * The add-on whose to-be-reset prefs are to be found.
+ */
+ _getAddonPrefs: function(addon) {
+ let entry = this._findMatchingAddonEntry(this._addonEntries, addon);
+ return entry.prefs.slice(0);
+ },
+
+ _findMatchingAddonEntry: function(aAddonEntries, aAddon) {
+ if (!aAddon)
+ return null;
+ // Returns true if the params object passes the constraints set by entry.
+ // (For every non-null property in entry, the same key must exist in
+ // params and value must be the same)
+ function checkEntry(entry, params) {
+ for (let [key, value] of entry) {
+ if (value === null || value === undefined)
+ continue;
+ if (params[key]) {
+ if (value instanceof RegExp) {
+ if (!value.test(params[key])) {
+ return false;
+ }
+ } else if (value !== params[key]) {
+ return false;
+ }
+ } else {
+ return false;
+ }
+ }
+ return true;
+ }
+
+ let params = {};
+ for (let filter of EXTENSION_BLOCK_FILTERS) {
+ params[filter] = aAddon[filter];
+ }
+ if (params.creator)
+ params.creator = params.creator.name;
+ for (let entry of aAddonEntries) {
+ if (checkEntry(entry.attributes, params)) {
+ return entry;
+ }
+ }
+ return null;
+ },
+
+ /* See nsIBlocklistService */
+ getAddonBlocklistURL: function(addon, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return "";
+
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let blItem = this._findMatchingAddonEntry(this._addonEntries, addon);
+ if (!blItem || !blItem.blockID)
+ return null;
+
+ return this._createBlocklistURL(blItem.blockID);
+ },
+
+ _createBlocklistURL: function(id) {
+ let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
+ url = url.replace(/%blockID%/g, id);
+
+ return url;
+ },
+
+ notify: function(aTimer) {
+ if (!gBlocklistEnabled)
+ return;
+
+ try {
+ var dsURI = gPref.getCharPref(PREF_BLOCKLIST_URL);
+ }
+ catch (e) {
+ LOG("Blocklist::notify: The " + PREF_BLOCKLIST_URL + " preference" +
+ " is missing!");
+ return;
+ }
+
+ var pingCountVersion = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTVERSION, 0);
+ var pingCountTotal = getPref("getIntPref", PREF_BLOCKLIST_PINGCOUNTTOTAL, 1);
+ var daysSinceLastPing = 0;
+ if (pingCountVersion == 0) {
+ daysSinceLastPing = "new";
+ }
+ else {
+ // Seconds in one day is used because nsIUpdateTimerManager stores the
+ // last update time in seconds.
+ let secondsInDay = 60 * 60 * 24;
+ let lastUpdateTime = getPref("getIntPref", PREF_BLOCKLIST_LASTUPDATETIME, 0);
+ if (lastUpdateTime == 0) {
+ daysSinceLastPing = "invalid";
+ }
+ else {
+ let now = Math.round(Date.now() / 1000);
+ daysSinceLastPing = Math.floor((now - lastUpdateTime) / secondsInDay);
+ }
+
+ if (daysSinceLastPing == 0 || daysSinceLastPing == "invalid") {
+ pingCountVersion = pingCountTotal = "invalid";
+ }
+ }
+
+ if (pingCountVersion < 1)
+ pingCountVersion = 1;
+ if (pingCountTotal < 1)
+ pingCountTotal = 1;
+
+ dsURI = dsURI.replace(/%APP_ID%/g, gApp.ID);
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (gApp.version)
+ dsURI = dsURI.replace(/%APP_VERSION%/g, gApp.version);
+ dsURI = dsURI.replace(/%PRODUCT%/g, gApp.name);
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (gApp.version)
+ dsURI = dsURI.replace(/%VERSION%/g, gApp.version);
+ dsURI = dsURI.replace(/%BUILD_ID%/g, gApp.appBuildID);
+ dsURI = dsURI.replace(/%BUILD_TARGET%/g, gApp.OS + "_" + gABI);
+ dsURI = dsURI.replace(/%OS_VERSION%/g, gOSVersion);
+ dsURI = dsURI.replace(/%LOCALE%/g, getLocale());
+ dsURI = dsURI.replace(/%CHANNEL%/g, UpdateUtils.UpdateChannel);
+ dsURI = dsURI.replace(/%PLATFORM_VERSION%/g, gApp.platformVersion);
+ dsURI = dsURI.replace(/%DISTRIBUTION%/g,
+ getDistributionPrefValue(PREF_APP_DISTRIBUTION));
+ dsURI = dsURI.replace(/%DISTRIBUTION_VERSION%/g,
+ getDistributionPrefValue(PREF_APP_DISTRIBUTION_VERSION));
+ dsURI = dsURI.replace(/%PING_COUNT%/g, pingCountVersion);
+ dsURI = dsURI.replace(/%TOTAL_PING_COUNT%/g, pingCountTotal);
+ dsURI = dsURI.replace(/%DAYS_SINCE_LAST_PING%/g, daysSinceLastPing);
+ dsURI = dsURI.replace(/\+/g, "%2B");
+
+ // Under normal operations it will take around 5,883,516 years before the
+ // preferences used to store pingCountVersion and pingCountTotal will rollover
+ // so this code doesn't bother trying to do the "right thing" here.
+ if (pingCountVersion != "invalid") {
+ pingCountVersion++;
+ if (pingCountVersion > 2147483647) {
+ // Rollover to -1 if the value is greater than what is support by an
+ // integer preference. The -1 indicates that the counter has been reset.
+ pingCountVersion = -1;
+ }
+ gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, pingCountVersion);
+ }
+
+ if (pingCountTotal != "invalid") {
+ pingCountTotal++;
+ if (pingCountTotal > 2147483647) {
+ // Rollover to 1 if the value is greater than what is support by an
+ // integer preference.
+ pingCountTotal = -1;
+ }
+ gPref.setIntPref(PREF_BLOCKLIST_PINGCOUNTTOTAL, pingCountTotal);
+ }
+
+ // Verify that the URI is valid
+ try {
+ var uri = newURI(dsURI);
+ }
+ catch (e) {
+ LOG("Blocklist::notify: There was an error creating the blocklist URI\r\n" +
+ "for: " + dsURI + ", error: " + e);
+ return;
+ }
+
+ LOG("Blocklist::notify: Requesting " + uri.spec);
+ let request = new ServiceRequest();
+ request.open("GET", uri.spec, true);
+ request.channel.notificationCallbacks = new gCertUtils.BadCertHandler();
+ request.overrideMimeType("text/xml");
+ request.setRequestHeader("Cache-Control", "no-cache");
+ request.QueryInterface(Components.interfaces.nsIJSXMLHttpRequest);
+
+ request.addEventListener("error", event => this.onXMLError(event), false);
+ request.addEventListener("load", event => this.onXMLLoad(event), false);
+ request.send(null);
+
+ // When the blocklist loads we need to compare it to the current copy so
+ // make sure we have loaded it.
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ // If kinto update is enabled, do the kinto update
+ if (gPref.getBoolPref(PREF_BLOCKLIST_UPDATE_ENABLED)) {
+ const updater =
+ Components.utils.import("resource://services-common/blocklist-updater.js",
+ {});
+ updater.checkVersions().catch(() => {
+ // Before we enable this in release, we want to collect telemetry on
+ // failed kinto updates - see bug 1254099
+ });
+ }
+ },
+
+ onXMLLoad: Task.async(function*(aEvent) {
+ let request = aEvent.target;
+ try {
+ gCertUtils.checkCert(request.channel);
+ }
+ catch (e) {
+ LOG("Blocklist::onXMLLoad: " + e);
+ return;
+ }
+ let responseXML = request.responseXML;
+ if (!responseXML || responseXML.documentElement.namespaceURI == XMLURI_PARSE_ERROR ||
+ (request.status != 200 && request.status != 0)) {
+ LOG("Blocklist::onXMLLoad: there was an error during load");
+ return;
+ }
+
+ var oldAddonEntries = this._addonEntries;
+ var oldPluginEntries = this._pluginEntries;
+ this._addonEntries = [];
+ this._gfxEntries = [];
+ this._pluginEntries = [];
+
+ this._loadBlocklistFromString(request.responseText);
+ // We don't inform the users when the graphics blocklist changed at runtime.
+ // However addons and plugins blocking status is refreshed.
+ this._blocklistUpdated(oldAddonEntries, oldPluginEntries);
+
+ try {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
+ yield OS.File.writeAtomic(path, request.responseText, {tmpPath: path + ".tmp"});
+ } catch (e) {
+ LOG("Blocklist::onXMLLoad: " + e);
+ }
+ }),
+
+ onXMLError: function(aEvent) {
+ try {
+ var request = aEvent.target;
+ // the following may throw (e.g. a local file or timeout)
+ var status = request.status;
+ }
+ catch (e) {
+ request = aEvent.target.channel.QueryInterface(Ci.nsIRequest);
+ status = request.status;
+ }
+ var statusText = "nsIXMLHttpRequest channel unavailable";
+ // When status is 0 we don't have a valid channel.
+ if (status != 0) {
+ try {
+ statusText = request.statusText;
+ } catch (e) {
+ }
+ }
+ LOG("Blocklist:onError: There was an error loading the blocklist file\r\n" +
+ statusText);
+ },
+
+ /**
+ * Finds the newest blocklist file from the application and the profile and
+ * load it or does nothing if neither exist.
+ */
+ _loadBlocklist: function() {
+ this._addonEntries = [];
+ this._gfxEntries = [];
+ this._pluginEntries = [];
+ var profFile = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+ if (profFile.exists()) {
+ this._loadBlocklistFromFile(profFile);
+ return;
+ }
+ var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ if (appFile.exists()) {
+ this._loadBlocklistFromFile(appFile);
+ return;
+ }
+ LOG("Blocklist::_loadBlocklist: no XML File found");
+ },
+
+ /**
+# The blocklist XML file looks something like this:
+#
+# <blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+# <emItems>
+# <emItem id="item_1@domain" blockID="i1">
+# <prefs>
+# <pref>accessibility.accesskeycausesactivation</pref>
+# <pref>accessibility.blockautorefresh</pref>
+# </prefs>
+# <versionRange minVersion="1.0" maxVersion="2.0.*">
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# <versionRange minVersion="1.7" maxVersion="1.7.*"/>
+# </targetApplication>
+# <targetApplication id="toolkit@mozilla.org">
+# <versionRange minVersion="1.9" maxVersion="1.9.*"/>
+# </targetApplication>
+# </versionRange>
+# <versionRange minVersion="3.0" maxVersion="3.0.*">
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# <targetApplication id="toolkit@mozilla.org">
+# <versionRange minVersion="1.9" maxVersion="1.9.*"/>
+# </targetApplication>
+# </versionRange>
+# </emItem>
+# <emItem id="item_2@domain" blockID="i2">
+# <versionRange minVersion="3.1" maxVersion="4.*"/>
+# </emItem>
+# <emItem id="item_3@domain">
+# <versionRange>
+# <targetApplication id="{ec8030f7-c20a-464f-9b0e-13a3a9e97384}">
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# </versionRange>
+# </emItem>
+# <emItem id="item_4@domain" blockID="i3">
+# <versionRange>
+# <targetApplication>
+# <versionRange minVersion="1.5" maxVersion="1.5.*"/>
+# </targetApplication>
+# </versionRange>
+# <emItem id="/@badperson\.com$/"/>
+# </emItems>
+# <pluginItems>
+# <pluginItem blockID="i4">
+# <!-- All match tags must match a plugin to blocklist a plugin -->
+# <match name="name" exp="some plugin"/>
+# <match name="description" exp="1[.]2[.]3"/>
+# </pluginItem>
+# </pluginItems>
+# <certItems>
+# <!-- issuerName is the DER issuer name data base64 encoded... -->
+# <certItem issuerName="MA0xCzAJBgNVBAMMAmNh">
+# <!-- ... as is the serial number DER data -->
+# <serialNumber>AkHVNA==</serialNumber>
+# </certItem>
+# <!-- subject is the DER subject name data base64 encoded... -->
+# <certItem subject="MA0xCzAJBgNVBAMMAmNh" pubKeyHash="/xeHA5s+i9/z9d8qy6JEuE1xGoRYIwgJuTE/lmaGJ7M=">
+# </certItem>
+# </certItems>
+# </blocklist>
+ */
+
+ _loadBlocklistFromFile: function(file) {
+ if (!gBlocklistEnabled) {
+ LOG("Blocklist::_loadBlocklistFromFile: blocklist is disabled");
+ return;
+ }
+
+ let telemetry = Services.telemetry;
+
+ if (this._isBlocklistPreloaded()) {
+ telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(false);
+ this._loadBlocklistFromString(this._preloadedBlocklistContent);
+ delete this._preloadedBlocklistContent;
+ return;
+ }
+
+ if (!file.exists()) {
+ LOG("Blocklist::_loadBlocklistFromFile: XML File does not exist " + file.path);
+ return;
+ }
+
+ telemetry.getHistogramById("BLOCKLIST_SYNC_FILE_LOAD").add(true);
+
+ let text = "";
+ let fstream = null;
+ let cstream = null;
+
+ try {
+ fstream = Components.classes["@mozilla.org/network/file-input-stream;1"]
+ .createInstance(Components.interfaces.nsIFileInputStream);
+ cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"]
+ .createInstance(Components.interfaces.nsIConverterInputStream);
+
+ fstream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ 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
+ text += str.value;
+ } while (read != 0);
+ } catch (e) {
+ LOG("Blocklist::_loadBlocklistFromFile: Failed to load XML file " + e);
+ } finally {
+ if (cstream)
+ cstream.close();
+ if (fstream)
+ fstream.close();
+ }
+
+ if (text)
+ this._loadBlocklistFromString(text);
+ },
+
+ _isBlocklistLoaded: function() {
+ return this._addonEntries != null && this._gfxEntries != null && this._pluginEntries != null;
+ },
+
+ _isBlocklistPreloaded: function() {
+ return this._preloadedBlocklistContent != null;
+ },
+
+ /* Used for testing */
+ _clear: function() {
+ this._addonEntries = null;
+ this._gfxEntries = null;
+ this._pluginEntries = null;
+ this._preloadedBlocklistContent = null;
+ },
+
+ _preloadBlocklist: Task.async(function*() {
+ let profPath = OS.Path.join(OS.Constants.Path.profileDir, FILE_BLOCKLIST);
+ try {
+ yield this._preloadBlocklistFile(profPath);
+ return;
+ } catch (e) {
+ LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
+ }
+
+ var appFile = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ try {
+ yield this._preloadBlocklistFile(appFile.path);
+ return;
+ } catch (e) {
+ LOG("Blocklist::_preloadBlocklist: Failed to load XML file " + e)
+ }
+
+ LOG("Blocklist::_preloadBlocklist: no XML File found");
+ }),
+
+ _preloadBlocklistFile: Task.async(function*(path) {
+ if (this._addonEntries) {
+ // The file has been already loaded.
+ return;
+ }
+
+ if (!gBlocklistEnabled) {
+ LOG("Blocklist::_preloadBlocklistFile: blocklist is disabled");
+ return;
+ }
+
+ let text = yield OS.File.read(path, { encoding: "utf-8" });
+
+ if (!this._addonEntries) {
+ // Store the content only if a sync load has not been performed in the meantime.
+ this._preloadedBlocklistContent = text;
+ }
+ }),
+
+ _loadBlocklistFromString : function(text) {
+ try {
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromString(text, "text/xml");
+ if (doc.documentElement.namespaceURI != XMLURI_BLOCKLIST) {
+ LOG("Blocklist::_loadBlocklistFromFile: aborting due to incorrect " +
+ "XML Namespace.\r\nExpected: " + XMLURI_BLOCKLIST + "\r\n" +
+ "Received: " + doc.documentElement.namespaceURI);
+ return;
+ }
+
+ var populateCertBlocklist = getPref("getBoolPref", PREF_ONECRL_VIA_AMO, true);
+
+ var childNodes = doc.documentElement.childNodes;
+ for (let element of childNodes) {
+ if (!(element instanceof Ci.nsIDOMElement))
+ continue;
+ switch (element.localName) {
+ case "emItems":
+ this._addonEntries = this._processItemNodes(element.childNodes, "emItem",
+ this._handleEmItemNode);
+ break;
+ case "pluginItems":
+ // We don't support plugins on b2g.
+ if (AppConstants.MOZ_B2G) {
+ return;
+ }
+ this._pluginEntries = this._processItemNodes(element.childNodes, "pluginItem",
+ this._handlePluginItemNode);
+ break;
+ case "certItems":
+ if (populateCertBlocklist) {
+ this._processItemNodes(element.childNodes, "certItem",
+ this._handleCertItemNode.bind(this));
+ }
+ break;
+ case "gfxItems":
+ // Parse as simple list of objects.
+ this._gfxEntries = this._processItemNodes(element.childNodes, "gfxBlacklistEntry",
+ this._handleGfxBlacklistNode);
+ break;
+ default:
+ LOG("Blocklist::_loadBlocklistFromString: ignored entries " + element.localName);
+ }
+ }
+ if (populateCertBlocklist) {
+ gCertBlocklistService.saveEntries();
+ }
+ if (this._gfxEntries.length > 0) {
+ this._notifyObserversBlocklistGFX();
+ }
+ }
+ catch (e) {
+ LOG("Blocklist::_loadBlocklistFromFile: Error constructing blocklist " + e);
+ return;
+ }
+ },
+
+ _processItemNodes: function(itemNodes, itemName, handler) {
+ var result = [];
+ for (var i = 0; i < itemNodes.length; ++i) {
+ var blocklistElement = itemNodes.item(i);
+ if (!(blocklistElement instanceof Ci.nsIDOMElement) ||
+ blocklistElement.localName != itemName)
+ continue;
+
+ handler(blocklistElement, result);
+ }
+ return result;
+ },
+
+ _handleCertItemNode: function(blocklistElement, result) {
+ let issuer = blocklistElement.getAttribute("issuerName");
+ if (issuer) {
+ for (let snElement of blocklistElement.children) {
+ try {
+ gCertBlocklistService.revokeCertByIssuerAndSerial(issuer, snElement.textContent);
+ } catch (e) {
+ // we want to keep trying other elements since missing all items
+ // is worse than missing one
+ LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Issuer and Serial" + e);
+ }
+ }
+ return;
+ }
+
+ let pubKeyHash = blocklistElement.getAttribute("pubKeyHash");
+ let subject = blocklistElement.getAttribute("subject");
+
+ if (pubKeyHash && subject) {
+ try {
+ gCertBlocklistService.revokeCertBySubjectAndPubKey(subject, pubKeyHash);
+ } catch (e) {
+ LOG("Blocklist::_handleCertItemNode: Error adding revoked cert by Subject and PubKey" + e);
+ }
+ }
+ },
+
+ _handleEmItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ let blockEntry = {
+ versions: [],
+ prefs: [],
+ blockID: null,
+ attributes: new Map()
+ // Atleast one of EXTENSION_BLOCK_FILTERS must get added to attributes
+ };
+
+ // Any filter starting with '/' is interpreted as a regex. So if an attribute
+ // starts with a '/' it must be checked via a regex.
+ function regExpCheck(attr) {
+ return attr.startsWith("/") ? parseRegExp(attr) : attr;
+ }
+
+ for (let filter of EXTENSION_BLOCK_FILTERS) {
+ let attr = blocklistElement.getAttribute(filter);
+ if (attr)
+ blockEntry.attributes.set(filter, regExpCheck(attr));
+ }
+
+ var childNodes = blocklistElement.childNodes;
+
+ for (let x = 0; x < childNodes.length; x++) {
+ var childElement = childNodes.item(x);
+ if (!(childElement instanceof Ci.nsIDOMElement))
+ continue;
+ if (childElement.localName === "prefs") {
+ let prefElements = childElement.childNodes;
+ for (let i = 0; i < prefElements.length; i++) {
+ let prefElement = prefElements.item(i);
+ if (!(prefElement instanceof Ci.nsIDOMElement) ||
+ prefElement.localName !== "pref")
+ continue;
+ blockEntry.prefs.push(prefElement.textContent);
+ }
+ }
+ else if (childElement.localName === "versionRange")
+ blockEntry.versions.push(new BlocklistItemData(childElement));
+ }
+ // if only the extension ID is specified block all versions of the
+ // extension for the current application.
+ if (blockEntry.versions.length == 0)
+ blockEntry.versions.push(new BlocklistItemData(null));
+
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+
+ result.push(blockEntry);
+ },
+
+ _handlePluginItemNode: function(blocklistElement, result) {
+ if (!matchesOSABI(blocklistElement))
+ return;
+
+ var matchNodes = blocklistElement.childNodes;
+ var blockEntry = {
+ matches: {},
+ versions: [],
+ blockID: null,
+ infoURL: null,
+ };
+ var hasMatch = false;
+ for (var x = 0; x < matchNodes.length; ++x) {
+ var matchElement = matchNodes.item(x);
+ if (!(matchElement instanceof Ci.nsIDOMElement))
+ continue;
+ if (matchElement.localName == "match") {
+ var name = matchElement.getAttribute("name");
+ var exp = matchElement.getAttribute("exp");
+ try {
+ blockEntry.matches[name] = new RegExp(exp, "m");
+ hasMatch = true;
+ } catch (e) {
+ // Ignore invalid regular expressions
+ }
+ }
+ if (matchElement.localName == "versionRange") {
+ blockEntry.versions.push(new BlocklistItemData(matchElement));
+ }
+ else if (matchElement.localName == "infoURL") {
+ blockEntry.infoURL = matchElement.textContent;
+ }
+ }
+ // Plugin entries require *something* to match to an actual plugin
+ if (!hasMatch)
+ return;
+ // Add a default versionRange if there wasn't one specified
+ if (blockEntry.versions.length == 0)
+ blockEntry.versions.push(new BlocklistItemData(null));
+
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+
+ result.push(blockEntry);
+ },
+
+ // <gfxBlacklistEntry blockID="g60">
+ // <os>WINNT 6.0</os>
+ // <osversion>14</osversion> currently only used for Android
+ // <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ // <vendor>0x8086</vendor>
+ // <devices>
+ // <device>0x2582</device>
+ // <device>0x2782</device>
+ // </devices>
+ // <feature> DIRECT3D_10_LAYERS </feature>
+ // <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ // <driverVersion> 8.52.322.2202 </driverVersion>
+ // <driverVersionMax> 8.52.322.2202 </driverVersionMax>
+ // <driverVersionComparator> LESS_THAN_OR_EQUAL </driverVersionComparator>
+ // <model>foo</model>
+ // <product>foo</product>
+ // <manufacturer>foo</manufacturer>
+ // <hardware>foo</hardware>
+ // </gfxBlacklistEntry>
+ _handleGfxBlacklistNode: function (blocklistElement, result) {
+ const blockEntry = {};
+
+ // The blockID attribute is always present in the actual data produced on server
+ // (see https://github.com/mozilla/addons-server/blob/2016.05.05/src/olympia/blocklist/templates/blocklist/blocklist.xml#L74)
+ // But it is sometimes missing in test fixtures.
+ if (blocklistElement.hasAttribute("blockID")) {
+ blockEntry.blockID = blocklistElement.getAttribute("blockID");
+ }
+
+ // Trim helper (spaces, tabs, no-break spaces..)
+ const trim = (s) => (s || '').replace(/(^[\s\uFEFF\xA0]+)|([\s\uFEFF\xA0]+$)/g, "");
+
+ for (let i = 0; i < blocklistElement.childNodes.length; ++i) {
+ var matchElement = blocklistElement.childNodes.item(i);
+ if (!(matchElement instanceof Ci.nsIDOMElement))
+ continue;
+
+ let value;
+ if (matchElement.localName == "devices") {
+ value = [];
+ for (let j = 0; j < matchElement.childNodes.length; j++) {
+ const childElement = matchElement.childNodes.item(j);
+ const childValue = trim(childElement.textContent);
+ // Make sure no empty value is added.
+ if (childValue) {
+ if (/,/.test(childValue)) {
+ // Devices can't contain comma.
+ // (c.f serialization in _notifyObserversBlocklistGFX)
+ const e = new Error(`Unsupported device name ${childValue}`);
+ Components.utils.reportError(e);
+ }
+ else {
+ value.push(childValue);
+ }
+ }
+ }
+ } else if (matchElement.localName == "versionRange") {
+ value = {minVersion: trim(matchElement.getAttribute("minVersion")) || "0",
+ maxVersion: trim(matchElement.getAttribute("maxVersion")) || "*"};
+ } else {
+ value = trim(matchElement.textContent);
+ }
+ if (value) {
+ blockEntry[matchElement.localName] = value;
+ }
+ }
+ result.push(blockEntry);
+ },
+
+ /* See nsIBlocklistService */
+ getPluginBlocklistState: function(plugin, appVersion, toolkitVersion) {
+ if (AppConstants.platform == "android" ||
+ AppConstants.MOZ_B2G) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+ return this._getPluginBlocklistState(plugin, this._pluginEntries,
+ appVersion, toolkitVersion);
+ },
+
+ /**
+ * Private helper to get the blocklist entry for a plugin given a set of
+ * blocklist entries and versions.
+ *
+ * @param plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns {entry: blocklistEntry, version: blocklistEntryVersion},
+ * or null if there is no matching entry.
+ */
+ _getPluginBlocklistEntry: function(plugin, pluginEntries, appVersion, toolkitVersion) {
+ if (!gBlocklistEnabled)
+ return null;
+
+ // Not all applications implement nsIXULAppInfo (e.g. xpcshell doesn't).
+ if (!appVersion && !gApp.version)
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+
+ if (!appVersion)
+ appVersion = gApp.version;
+ if (!toolkitVersion)
+ toolkitVersion = gApp.platformVersion;
+
+ for (var blockEntry of pluginEntries) {
+ var matchFailed = false;
+ for (var name in blockEntry.matches) {
+ if (!(name in plugin) ||
+ typeof(plugin[name]) != "string" ||
+ !blockEntry.matches[name].test(plugin[name])) {
+ matchFailed = true;
+ break;
+ }
+ }
+
+ if (matchFailed)
+ continue;
+
+ for (let blockEntryVersion of blockEntry.versions) {
+ if (blockEntryVersion.includesItem(plugin.version, appVersion,
+ toolkitVersion)) {
+ return {entry: blockEntry, version: blockEntryVersion};
+ }
+ }
+ }
+
+ return null;
+ },
+
+ /**
+ * Private version of getPluginBlocklistState that allows the caller to pass in
+ * the plugin blocklist entries.
+ *
+ * @param plugin
+ * The nsIPluginTag to get the blocklist state for.
+ * @param pluginEntries
+ * The plugin blocklist entries to compare against.
+ * @param appVersion
+ * The application version to compare to, will use the current
+ * version if null.
+ * @param toolkitVersion
+ * The toolkit version to compare to, will use the current version if
+ * null.
+ * @returns The blocklist state for the item, one of the STATE constants as
+ * defined in nsIBlocklistService.
+ */
+ _getPluginBlocklistState: function(plugin, pluginEntries, appVersion, toolkitVersion) {
+
+ let r = this._getPluginBlocklistEntry(plugin, pluginEntries,
+ appVersion, toolkitVersion);
+ if (!r) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ }
+
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+
+ if (blockEntryVersion.severity >= gBlocklistLevel)
+ return Ci.nsIBlocklistService.STATE_BLOCKED;
+ if (blockEntryVersion.severity == SEVERITY_OUTDATED) {
+ let vulnerabilityStatus = blockEntryVersion.vulnerabilityStatus;
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_UPDATE_AVAILABLE)
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE;
+ if (vulnerabilityStatus == VULNERABILITYSTATUS_NO_UPDATE)
+ return Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE;
+ return Ci.nsIBlocklistService.STATE_OUTDATED;
+ }
+ return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ },
+
+ /* See nsIBlocklistService */
+ getPluginBlocklistURL: function(plugin) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
+ if (!r) {
+ return null;
+ }
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+ if (!blockEntry.blockID) {
+ return null;
+ }
+
+ return this._createBlocklistURL(blockEntry.blockID);
+ },
+
+ /* See nsIBlocklistService */
+ getPluginInfoURL: function(plugin) {
+ if (!this._isBlocklistLoaded())
+ this._loadBlocklist();
+
+ let r = this._getPluginBlocklistEntry(plugin, this._pluginEntries);
+ if (!r) {
+ return null;
+ }
+ let {entry: blockEntry, version: blockEntryVersion} = r;
+ if (!blockEntry.blockID) {
+ return null;
+ }
+
+ return blockEntry.infoURL;
+ },
+
+ _notifyObserversBlocklistGFX: function () {
+ // Notify `GfxInfoBase`, by passing a string serialization.
+ // This way we avoid spreading XML structure logics there.
+ const payload = this._gfxEntries.map((r) => {
+ return Object.keys(r).sort().filter((k) => !/id|last_modified/.test(k)).map((key) => {
+ let value = r[key];
+ if (Array.isArray(value)) {
+ value = value.join(",");
+ } else if (value.hasOwnProperty("minVersion")) {
+ // When XML is parsed, both minVersion and maxVersion are set.
+ value = `${value.minVersion},${value.maxVersion}`;
+ }
+ return `${key}:${value}`;
+ }).join("\t");
+ }).join("\n");
+ Services.obs.notifyObservers(null, "blocklist-data-gfxItems", payload);
+ },
+
+ _notifyObserversBlocklistUpdated: function() {
+ Services.obs.notifyObservers(this, "blocklist-updated", "");
+ Services.ppmm.broadcastAsyncMessage("Blocklist:blocklistInvalidated", {});
+ },
+
+ _blocklistUpdated: function(oldAddonEntries, oldPluginEntries) {
+ if (AppConstants.MOZ_B2G) {
+ return;
+ }
+
+ var addonList = [];
+
+ // A helper function that reverts the prefs passed to default values.
+ function resetPrefs(prefs) {
+ for (let pref of prefs)
+ gPref.clearUserPref(pref);
+ }
+ const types = ["extension", "theme", "locale", "dictionary", "service"];
+ AddonManager.getAddonsByTypes(types, addons => {
+ for (let addon of addons) {
+ let oldState = Ci.nsIBlocklistService.STATE_NOTBLOCKED;
+ if (oldAddonEntries)
+ oldState = this._getAddonBlocklistState(addon, oldAddonEntries);
+ let state = this.getAddonBlocklistState(addon);
+
+ LOG("Blocklist state for " + addon.id + " changed from " +
+ oldState + " to " + state);
+
+ // We don't want to re-warn about add-ons
+ if (state == oldState)
+ continue;
+
+ if (state === Ci.nsIBlocklistService.STATE_BLOCKED) {
+ // It's a hard block. We must reset certain preferences.
+ let prefs = this._getAddonPrefs(addon);
+ resetPrefs(prefs);
+ }
+
+ // Ensure that softDisabled is false if the add-on is not soft blocked
+ if (state != Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ addon.softDisabled = false;
+
+ // Don't warn about add-ons becoming unblocked.
+ if (state == Ci.nsIBlocklistService.STATE_NOT_BLOCKED)
+ continue;
+
+ // If an add-on has dropped from hard to soft blocked just mark it as
+ // soft disabled and don't warn about it.
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED &&
+ oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ addon.softDisabled = true;
+ continue;
+ }
+
+ // If the add-on is already disabled for some reason then don't warn
+ // about it
+ if (!addon.isActive) {
+ // But mark it as softblocked if necessary. Note that we avoid setting
+ // softDisabled at the same time as userDisabled to make it clear
+ // which was the original cause of the add-on becoming disabled in a
+ // way that the user can change.
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED && !addon.userDisabled)
+ addon.softDisabled = true;
+ continue;
+ }
+
+ addonList.push({
+ name: addon.name,
+ version: addon.version,
+ icon: addon.iconURL,
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: addon,
+ url: this.getAddonBlocklistURL(addon),
+ });
+ }
+
+ AddonManagerPrivate.updateAddonAppDisabledStates();
+
+ var phs = Cc["@mozilla.org/plugin/host;1"].
+ getService(Ci.nsIPluginHost);
+ var plugins = phs.getPluginTags();
+
+ for (let plugin of plugins) {
+ let oldState = -1;
+ if (oldPluginEntries)
+ oldState = this._getPluginBlocklistState(plugin, oldPluginEntries);
+ let state = this.getPluginBlocklistState(plugin);
+ LOG("Blocklist state for " + plugin.name + " changed from " +
+ oldState + " to " + state);
+ // We don't want to re-warn about items
+ if (state == oldState)
+ continue;
+
+ if (oldState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ if (state == Ci.nsIBlocklistService.STATE_SOFTBLOCKED)
+ plugin.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ }
+ else if (!plugin.disabled && state != Ci.nsIBlocklistService.STATE_NOT_BLOCKED) {
+ if (state != Ci.nsIBlocklistService.STATE_OUTDATED &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE &&
+ state != Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE) {
+ addonList.push({
+ name: plugin.name,
+ version: plugin.version,
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: state == Ci.nsIBlocklistService.STATE_BLOCKED,
+ item: plugin,
+ url: this.getPluginBlocklistURL(plugin),
+ });
+ }
+ }
+ }
+
+ if (addonList.length == 0) {
+ this._notifyObserversBlocklistUpdated();
+ return;
+ }
+
+ if ("@mozilla.org/addons/blocklist-prompt;1" in Cc) {
+ try {
+ let blockedPrompter = Cc["@mozilla.org/addons/blocklist-prompt;1"]
+ .getService(Ci.nsIBlocklistPrompt);
+ blockedPrompter.prompt(addonList);
+ } catch (e) {
+ LOG(e);
+ }
+ this._notifyObserversBlocklistUpdated();
+ return;
+ }
+
+ var args = {
+ restart: false,
+ list: addonList
+ };
+ // This lets the dialog get the raw js object
+ args.wrappedJSObject = args;
+
+ /*
+ Some tests run without UI, so the async code listens to a message
+ that can be sent programatically
+ */
+ let applyBlocklistChanges = () => {
+ for (let addon of addonList) {
+ if (!addon.disable)
+ continue;
+
+ if (addon.item instanceof Ci.nsIPluginTag)
+ addon.item.enabledState = Ci.nsIPluginTag.STATE_DISABLED;
+ else {
+ // This add-on is softblocked.
+ addon.item.softDisabled = true;
+ // We must revert certain prefs.
+ let prefs = this._getAddonPrefs(addon.item);
+ resetPrefs(prefs);
+ }
+ }
+
+ if (args.restart)
+ restartApp();
+
+ this._notifyObserversBlocklistUpdated();
+ Services.obs.removeObserver(applyBlocklistChanges, "addon-blocklist-closed");
+ }
+
+ Services.obs.addObserver(applyBlocklistChanges, "addon-blocklist-closed", false);
+
+ if (getPref("getBoolPref", PREF_BLOCKLIST_SUPPRESSUI, false)) {
+ applyBlocklistChanges();
+ return;
+ }
+
+ function blocklistUnloadHandler(event) {
+ if (event.target.location == URI_BLOCKLIST_DIALOG) {
+ applyBlocklistChanges();
+ blocklistWindow.removeEventListener("unload", blocklistUnloadHandler);
+ }
+ }
+
+ let blocklistWindow = Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+ "chrome,centerscreen,dialog,titlebar", args);
+ if (blocklistWindow)
+ blocklistWindow.addEventListener("unload", blocklistUnloadHandler, false);
+ });
+ },
+
+ classID: Components.ID("{66354bc9-7ed1-4692-ae1d-8da97d6b205e}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIBlocklistService,
+ Ci.nsITimerCallback]),
+};
+
+/**
+ * Helper for constructing a blocklist.
+ */
+function BlocklistItemData(versionRangeElement) {
+ var versionRange = this.getBlocklistVersionRange(versionRangeElement);
+ this.minVersion = versionRange.minVersion;
+ this.maxVersion = versionRange.maxVersion;
+ if (versionRangeElement && versionRangeElement.hasAttribute("severity"))
+ this.severity = versionRangeElement.getAttribute("severity");
+ else
+ this.severity = DEFAULT_SEVERITY;
+ if (versionRangeElement && versionRangeElement.hasAttribute("vulnerabilitystatus")) {
+ this.vulnerabilityStatus = versionRangeElement.getAttribute("vulnerabilitystatus");
+ } else {
+ this.vulnerabilityStatus = VULNERABILITYSTATUS_NONE;
+ }
+ this.targetApps = { };
+ var found = false;
+
+ if (versionRangeElement) {
+ for (var i = 0; i < versionRangeElement.childNodes.length; ++i) {
+ var targetAppElement = versionRangeElement.childNodes.item(i);
+ if (!(targetAppElement instanceof Ci.nsIDOMElement) ||
+ targetAppElement.localName != "targetApplication")
+ continue;
+ found = true;
+ // default to the current application if id is not provided.
+ var appID = targetAppElement.hasAttribute("id") ? targetAppElement.getAttribute("id") : gApp.ID;
+ this.targetApps[appID] = this.getBlocklistAppVersions(targetAppElement);
+ }
+ }
+ // Default to all versions of the current application when no targetApplication
+ // elements were found
+ if (!found)
+ this.targetApps[gApp.ID] = this.getBlocklistAppVersions(null);
+}
+
+BlocklistItemData.prototype = {
+ /**
+ * Tests if a version of an item is included in the version range and target
+ * application information represented by this BlocklistItemData using the
+ * provided application and toolkit versions.
+ * @param version
+ * The version of the item being tested.
+ * @param appVersion
+ * The application version to test with.
+ * @param toolkitVersion
+ * The toolkit version to test with.
+ * @returns True if the version range covers the item version and application
+ * or toolkit version.
+ */
+ includesItem: function(version, appVersion, toolkitVersion) {
+ // Some platforms have no version for plugins, these don't match if there
+ // was a min/maxVersion provided
+ if (!version && (this.minVersion || this.maxVersion))
+ return false;
+
+ // Check if the item version matches
+ if (!this.matchesRange(version, this.minVersion, this.maxVersion))
+ return false;
+
+ // Check if the application version matches
+ if (this.matchesTargetRange(gApp.ID, appVersion))
+ return true;
+
+ // Check if the toolkit version matches
+ return this.matchesTargetRange(TOOLKIT_ID, toolkitVersion);
+ },
+
+ /**
+ * Checks if a version is higher than or equal to the minVersion (if provided)
+ * and lower than or equal to the maxVersion (if provided).
+ * @param version
+ * The version to test.
+ * @param minVersion
+ * The minimum version. If null it is assumed that version is always
+ * larger.
+ * @param maxVersion
+ * The maximum version. If null it is assumed that version is always
+ * smaller.
+ */
+ matchesRange: function(version, minVersion, maxVersion) {
+ if (minVersion && gVersionChecker.compare(version, minVersion) < 0)
+ return false;
+ if (maxVersion && gVersionChecker.compare(version, maxVersion) > 0)
+ return false;
+ return true;
+ },
+
+ /**
+ * Tests if there is a matching range for the given target application id and
+ * version.
+ * @param appID
+ * The application ID to test for, may be for an application or toolkit
+ * @param appVersion
+ * The version of the application to test for.
+ * @returns True if this version range covers the application version given.
+ */
+ matchesTargetRange: function(appID, appVersion) {
+ var blTargetApp = this.targetApps[appID];
+ if (!blTargetApp)
+ return false;
+
+ for (let app of blTargetApp) {
+ if (this.matchesRange(appVersion, app.minVersion, app.maxVersion))
+ return true;
+ }
+
+ return false;
+ },
+
+ /**
+ * Retrieves a version range (e.g. minVersion and maxVersion) for a
+ * blocklist item's targetApplication element.
+ * @param targetAppElement
+ * A targetApplication blocklist element.
+ * @returns An array of JS objects with the following properties:
+ * "minVersion" The minimum version in a version range (default = null).
+ * "maxVersion" The maximum version in a version range (default = null).
+ */
+ getBlocklistAppVersions: function(targetAppElement) {
+ var appVersions = [ ];
+
+ if (targetAppElement) {
+ for (var i = 0; i < targetAppElement.childNodes.length; ++i) {
+ var versionRangeElement = targetAppElement.childNodes.item(i);
+ if (!(versionRangeElement instanceof Ci.nsIDOMElement) ||
+ versionRangeElement.localName != "versionRange")
+ continue;
+ appVersions.push(this.getBlocklistVersionRange(versionRangeElement));
+ }
+ }
+ // return minVersion = null and maxVersion = null if no specific versionRange
+ // elements were found
+ if (appVersions.length == 0)
+ appVersions.push(this.getBlocklistVersionRange(null));
+ return appVersions;
+ },
+
+ /**
+ * Retrieves a version range (e.g. minVersion and maxVersion) for a blocklist
+ * versionRange element.
+ * @param versionRangeElement
+ * The versionRange blocklist element.
+ * @returns A JS object with the following properties:
+ * "minVersion" The minimum version in a version range (default = null).
+ * "maxVersion" The maximum version in a version range (default = null).
+ */
+ getBlocklistVersionRange: function(versionRangeElement) {
+ var minVersion = null;
+ var maxVersion = null;
+ if (!versionRangeElement)
+ return { minVersion: minVersion, maxVersion: maxVersion };
+
+ if (versionRangeElement.hasAttribute("minVersion"))
+ minVersion = versionRangeElement.getAttribute("minVersion");
+ if (versionRangeElement.hasAttribute("maxVersion"))
+ maxVersion = versionRangeElement.getAttribute("maxVersion");
+
+ return { minVersion: minVersion, maxVersion: maxVersion };
+ }
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([Blocklist]);
diff --git a/toolkit/mozapps/extensions/nsBlocklistServiceContent.js b/toolkit/mozapps/extensions/nsBlocklistServiceContent.js
new file mode 100644
index 000000000..1752924b5
--- /dev/null
+++ b/toolkit/mozapps/extensions/nsBlocklistServiceContent.js
@@ -0,0 +1,113 @@
+/* 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/mozapps/extensions/test/AddonManagerTesting.jsm b/toolkit/mozapps/extensions/test/AddonManagerTesting.jsm
new file mode 100644
index 000000000..eb9a92195
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/AddonManagerTesting.jsm
@@ -0,0 +1,115 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+// This file is a test-only JSM containing utility methods for
+// interacting with the add-ons manager.
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+ "AddonManagerTesting",
+];
+
+const {utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "AddonManager",
+ "resource://gre/modules/AddonManager.jsm");
+
+this.AddonManagerTesting = {
+ /**
+ * Get the add-on that is specified by its ID.
+ *
+ * @return {Promise<Object>} A promise that resolves returning the found addon or null
+ * if it is not found.
+ */
+ getAddonById: function (id) {
+ return new Promise(resolve => AddonManager.getAddonByID(id, addon => resolve(addon)));
+ },
+
+ /**
+ * Uninstall an add-on that is specified by its ID.
+ *
+ * The returned promise resolves on successful uninstall and rejects
+ * if the add-on is not unknown.
+ *
+ * @return Promise<restartRequired>
+ */
+ uninstallAddonByID: function (id) {
+ let deferred = Promise.defer();
+
+ AddonManager.getAddonByID(id, (addon) => {
+ if (!addon) {
+ deferred.reject(new Error("Add-on is not known: " + id));
+ return;
+ }
+
+ let listener = {
+ onUninstalling: function (addon, needsRestart) {
+ if (addon.id != id) {
+ return;
+ }
+
+ if (needsRestart) {
+ AddonManager.removeAddonListener(listener);
+ deferred.resolve(true);
+ }
+ },
+
+ onUninstalled: function (addon) {
+ if (addon.id != id) {
+ return;
+ }
+
+ AddonManager.removeAddonListener(listener);
+ deferred.resolve(false);
+ },
+
+ onOperationCancelled: function (addon) {
+ if (addon.id != id) {
+ return;
+ }
+
+ AddonManager.removeAddonListener(listener);
+ deferred.reject(new Error("Uninstall cancelled."));
+ },
+ };
+
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+ });
+
+ return deferred.promise;
+ },
+
+ /**
+ * Install an XPI add-on from a URL.
+ *
+ * @return Promise<addon>
+ */
+ installXPIFromURL: function (url, hash, name, iconURL, version) {
+ let deferred = Promise.defer();
+
+ AddonManager.getInstallForURL(url, (install) => {
+ let fail = () => { deferred.reject(new Error("Add-on install failed.")) };
+
+ let listener = {
+ onDownloadCancelled: fail,
+ onDownloadFailed: fail,
+ onInstallCancelled: fail,
+ onInstallFailed: fail,
+ onInstallEnded: function (install, addon) {
+ deferred.resolve(addon);
+ },
+ };
+
+ install.addListener(listener);
+ install.install();
+ }, "application/x-xpinstall", hash, name, iconURL, version);
+
+ return deferred.promise;
+ },
+};
diff --git a/toolkit/mozapps/extensions/test/Makefile.in b/toolkit/mozapps/extensions/test/Makefile.in
new file mode 100644
index 000000000..6c667ecab
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/Makefile.in
@@ -0,0 +1,20 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+ADDONSRC = $(srcdir)/addons
+TESTROOT = $(CURDIR)/$(DEPTH)/_tests/xpcshell/$(relativesrcdir)
+TESTXPI = $(TESTROOT)/xpcshell/addons
+
+include $(topsrcdir)/config/rules.mk
+
+libs::
+ rm -rf $(TESTXPI)
+ $(NSINSTALL) -D $(TESTXPI)
+ if [ -d $(ADDONSRC) ]; then \
+ $(EXIT_ON_ERROR) \
+ for dir in $(ADDONSRC)/*; do \
+ base=`basename $$dir` ; \
+ (cd $$dir && zip -qr $(TESTXPI)/$$base.xpi *) \
+ done \
+ fi
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_hard1_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_1/install.rdf
new file mode 100644
index 000000000..7b1b02a17
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>hardblock@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Hardblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_hard1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_2/install.rdf
new file mode 100644
index 000000000..ae364637e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>hardblock@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Hardblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_hard1_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_3/install.rdf
new file mode 100644
index 000000000..568c41a43
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_hard1_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>hardblock@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Hardblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_1/install.rdf
new file mode 100644
index 000000000..1281ab53f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>regexpblock@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>RegExp-blocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_2/install.rdf
new file mode 100644
index 000000000..8b6dd09f5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>regexpblock@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>RegExp-blocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_3/install.rdf
new file mode 100644
index 000000000..fade395f9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_regexp1_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>regexpblock@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>RegExp-blocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft1_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_1/install.rdf
new file mode 100644
index 000000000..4a18f64e0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_2/install.rdf
new file mode 100644
index 000000000..8a2519222
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft1_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_3/install.rdf
new file mode 100644
index 000000000..2c55e5ff7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft1_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock1@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft2_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_1/install.rdf
new file mode 100644
index 000000000..eebac4b21
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft2_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_2/install.rdf
new file mode 100644
index 000000000..f37741d04
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock2@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft2_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_3/install.rdf
new file mode 100644
index 000000000..e15f99264
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft2_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock2@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft3_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_1/install.rdf
new file mode 100644
index 000000000..f4b70a24b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft3_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_2/install.rdf
new file mode 100644
index 000000000..987204fa6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock3@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft3_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_3/install.rdf
new file mode 100644
index 000000000..19ab4b9fe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft3_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock3@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft4_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_1/install.rdf
new file mode 100644
index 000000000..a3cd06f5f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_1/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock4@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft4_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_2/install.rdf
new file mode 100644
index 000000000..eeff9fb79
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_2/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock4@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft4_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_3/install.rdf
new file mode 100644
index 000000000..1d2650603
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft4_3/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>softblock4@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft5_1/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_1/install.rdf
new file mode 100644
index 000000000..85d7108d6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_1/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+
+<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>softblock5@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:internalName>test/1.0</em:internalName>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft5_2/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_2/install.rdf
new file mode 100644
index 000000000..394fd909e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_2/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+
+<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>softblock5@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:internalName>test/1.0</em:internalName>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/blocklist_soft5_3/install.rdf b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_3/install.rdf
new file mode 100644
index 000000000..2a1fec25a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/blocklist_soft5_3/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+
+<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>softblock5@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:name>Softblocked add-on</em:name>
+ <em:internalName>test/1.0</em:internalName>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/bootstrap_globals/bootstrap.js b/toolkit/mozapps/extensions/test/addons/bootstrap_globals/bootstrap.js
new file mode 100644
index 000000000..a5d5beb34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/bootstrap_globals/bootstrap.js
@@ -0,0 +1,29 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+var seenGlobals = new Set();
+var scope = this;
+function checkGlobal(name, type) {
+ if (scope[name] && typeof(scope[name]) == type)
+ seenGlobals.add(name);
+}
+
+var wrapped = {};
+Services.obs.notifyObservers({ wrappedJSObject: wrapped }, "bootstrap-request-globals", null);
+for (let [name, type] of wrapped.expectedGlobals) {
+ checkGlobal(name, type);
+}
+
+function install(data, reason) {
+}
+
+function startup(data, reason) {
+ Services.obs.notifyObservers({
+ wrappedJSObject: seenGlobals
+ }, "bootstrap-seen-globals", null);
+}
+
+function shutdown(data, reason) {
+}
+
+function uninstall(data, reason) {
+}
diff --git a/toolkit/mozapps/extensions/test/addons/bootstrap_globals/install.rdf b/toolkit/mozapps/extensions/test/addons/bootstrap_globals/install.rdf
new file mode 100644
index 000000000..f11a626fd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/bootstrap_globals/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>bootstrap_globals@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap Globals</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/min1max1/install.rdf b/toolkit/mozapps/extensions/test/addons/min1max1/install.rdf
new file mode 100644
index 000000000..3a0ace227
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/min1max1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>min1max1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test minVersion 1 maxVersion 1</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/min1max2/install.rdf b/toolkit/mozapps/extensions/test/addons/min1max2/install.rdf
new file mode 100644
index 000000000..0184f1963
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/min1max2/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>min1max2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test minVersion 1 maxVersion 2</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/min1max3/install.rdf b/toolkit/mozapps/extensions/test/addons/min1max3/install.rdf
new file mode 100644
index 000000000..dbb1b7318
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/min1max3/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>min1max3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test minVersion 1 maxVersion 3</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/min1max3b/install.rdf b/toolkit/mozapps/extensions/test/addons/min1max3b/install.rdf
new file mode 100644
index 000000000..f50c65c6a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/min1max3b/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>min1max3b@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Another Test minVersion 1 maxVersion 3</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/override1x2-1x3/install.rdf b/toolkit/mozapps/extensions/test/addons/override1x2-1x3/install.rdf
new file mode 100644
index 000000000..92cf3ec96
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/override1x2-1x3/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>override1x2-1x3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:updateURL>http://localhost:4444/data/test_bug542391.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test override compat from 1..2 to 1..3</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_AddonRepository_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_1/install.rdf
new file mode 100644
index 000000000..82cfd0472
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_1/install.rdf
@@ -0,0 +1,33 @@
+<?xml version="1.0"?>
+
+<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>test_AddonRepository_1@tests.mozilla.org</em:id>
+ <em:version>1.1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Add-on 1</em:name>
+ <em:description>XPI Add-on 1 - Description</em:description>
+ <em:creator>XPI Add-on 1 - Creator</em:creator>
+ <em:developer>XPI Add-on 1 - First Developer</em:developer>
+ <em:developer>XPI Add-on 1 - Second Developer</em:developer>
+ <em:translator>XPI Add-on 1 - First Translator</em:translator>
+ <em:translator>XPI Add-on 1 - Second Translator</em:translator>
+ <em:contributor>XPI Add-on 1 - First Contributor</em:contributor>
+ <em:contributor>XPI Add-on 1 - Second Contributor</em:contributor>
+ <em:homepageURL>http://localhost/xpi/1/homepage.html</em:homepageURL>
+ <em:optionsURL>http://localhost/xpi/1/options.html</em:optionsURL>
+ <em:aboutURL>http://localhost/xpi/1/about.html</em:aboutURL>
+ <em:iconURL>http://localhost/xpi/1/icon.png</em:iconURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_AddonRepository_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_2/install.rdf
new file mode 100644
index 000000000..80776e6c3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>test_AddonRepository_2@tests.mozilla.org</em:id>
+ <em:type>4</em:type>
+ <em:internalName>test2/1.0</em:internalName>
+ <em:version>1.2</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Add-on 2</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/icon.png b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/icon.png
new file mode 100644
index 000000000..41409edfe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/icon.png
@@ -0,0 +1 @@
+Fake icon image
diff --git a/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/install.rdf
new file mode 100644
index 000000000..bade9c069
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>test_AddonRepository_3@tests.mozilla.org</em:id>
+ <em:type>4</em:type>
+ <em:internalName>test3/1.0</em:internalName>
+ <em:version>1.3</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Add-on 3</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/preview.png b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/preview.png
new file mode 100644
index 000000000..321ce47cf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_AddonRepository_3/preview.png
@@ -0,0 +1 @@
+Fake preview image
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/install.rdf
new file mode 100644
index 000000000..f02a3869c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm
new file mode 100644
index 000000000..7fe60e632
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_1/version.jsm
@@ -0,0 +1,3 @@
+this.EXPORTED_SYMBOLS = ["VERSION"];
+
+this.VERSION = 1;
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/install.rdf
new file mode 100644
index 000000000..480f03fd1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm
new file mode 100644
index 000000000..532741e12
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_2/version.jsm
@@ -0,0 +1,3 @@
+this.EXPORTED_SYMBOLS = ["VERSION"];
+
+this.VERSION = 2;
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/install.rdf
new file mode 100644
index 000000000..e9385cbb3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>undefined</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm
new file mode 100644
index 000000000..1b813faaf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_3/version.jsm
@@ -0,0 +1,3 @@
+this.EXPORTED_SYMBOLS = ["VERSION"];
+
+this.VERSION = 3;
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap1_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_4/install.rdf
new file mode 100644
index 000000000..2b88e0ad0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap1_4/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>4.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/install.rdf
new file mode 100644
index 000000000..e0e8ca978
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap2_1/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>bootstrap2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 2</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/bootstrap.js
new file mode 100644
index 000000000..0e45caec4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/bootstrap.js
@@ -0,0 +1,5 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const install = function() {
+ Services.obs.notifyObservers(null, "addon-install", "");
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/install.rdf
new file mode 100644
index 000000000..898282991
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bootstrap_const/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bootstrap@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_2/install.rdf
new file mode 100644
index 000000000..791a6263f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-2@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- XPCShell -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Toolkit -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716_2.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_a_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_a_1/install.rdf
new file mode 100644
index 000000000..36d15b8aa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_a_1/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<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>bug299716-a@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test A</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_a_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_a_2/install.rdf
new file mode 100644
index 000000000..3521a503c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_a_2/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<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>bug299716-a@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test A</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_b_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_b_1/install.rdf
new file mode 100644
index 000000000..d92a4ec41
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_b_1/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<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>bug299716-b@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test B</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_b_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_b_2/install.rdf
new file mode 100644
index 000000000..c3ad76b84
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_b_2/install.rdf
@@ -0,0 +1,20 @@
+<?xml version="1.0"?>
+
+<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>bug299716-b@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test B</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_c_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_c_1/install.rdf
new file mode 100644
index 000000000..a937b6e76
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_c_1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-c@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- XPCShell -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Toolkit -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test C</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_c_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_c_2/install.rdf
new file mode 100644
index 000000000..8afca3ff9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_c_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-c@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <!-- XPCShell -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Toolkit -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test C</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_d_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_d_1/install.rdf
new file mode 100644
index 000000000..4c0dcc2ef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_d_1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-d@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- XPCShell -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test D</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_d_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_d_2/install.rdf
new file mode 100644
index 000000000..2b113809a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_d_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-d@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <!-- XPCShell -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test D</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_e_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_e_1/install.rdf
new file mode 100644
index 000000000..03eb7180e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_e_1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-e@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- Toolkit -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- XPCShell, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test E</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_e_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_e_2/install.rdf
new file mode 100644
index 000000000..3ed7cd932
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_e_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-e@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <!-- Toolkit -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- XPCShell, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test E</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_f_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_f_1/install.rdf
new file mode 100644
index 000000000..cacf824c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_f_1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-f@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- XPCShell, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test F</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_f_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_f_2/install.rdf
new file mode 100644
index 000000000..09954ec36
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_f_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug299716-f@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- XPCShell, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test F</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_g_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_g_1/install.rdf
new file mode 100644
index 000000000..5e4a6f6a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_g_1/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<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>bug299716-g@tests.mozilla.org</em:id>
+ <em:version>0.1</em:version>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test G</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug299716_g_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug299716_g_2/install.rdf
new file mode 100644
index 000000000..913233cec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug299716_g_2/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<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>bug299716-g@tests.mozilla.org</em:id>
+ <em:version>0.2</em:version>
+
+ <!-- Toolkit, invalid -->
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 299716 test G</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug299716.rdf</em:updateURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_1/install.rdf
new file mode 100644
index 000000000..fd0dd50b7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_1/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 1</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121_1.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_2/install.rdf
new file mode 100644
index 000000000..607b68357
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_2/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_2@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 2</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_3/install.rdf
new file mode 100644
index 000000000..3a4c7eafc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_3/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_3@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 5</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_4/install.rdf
new file mode 100644
index 000000000..8557df116
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_4/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_4@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 4</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121_4.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_5/install.rdf
new file mode 100644
index 000000000..343a9d44c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_5/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_5@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 5</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121_5.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_6/install.rdf
new file mode 100644
index 000000000..5a724cc99
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_6/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_6@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 6</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_7/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_7/install.rdf
new file mode 100644
index 000000000..70fe81168
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_7/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_7@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 7</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_8/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_8/install.rdf
new file mode 100644
index 000000000..2aface3b4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_8/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_8@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 8</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121_8.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug324121_9/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug324121_9/install.rdf
new file mode 100644
index 000000000..7804e833c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug324121_9/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<!-- Compatible to install -->
+
+<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>bug324121_9@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 324121 Test 9</em:name>
+ <em:updateURL>http://localhost:4444/data/test_bug324121_9.rdf</em:updateURL>
+
+ </Description>
+</RDF>
+
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug335238_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug335238_1/install.rdf
new file mode 100644
index 000000000..c60b5711b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug335238_1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>bug335238_1@tests.mozilla.org</em:id>
+ <em:version>1.3.4</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 335238</em:name>
+ <em:updateURL>http://localhost:4444/0?id=%ITEM_ID%&amp;version=%ITEM_VERSION%&amp;maxAppVersion=%ITEM_MAXAPPVERSION%&amp;status=%ITEM_STATUS%&amp;appId=%APP_ID%&amp;appVersion=%APP_VERSION%&amp;appOs=%APP_OS%&amp;appAbi=%APP_ABI%&amp;locale=%APP_LOCALE%&amp;reqVersion=%REQ_VERSION%</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug335238_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug335238_2/install.rdf
new file mode 100644
index 000000000..23faf5a34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug335238_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug335238_2@tests.mozilla.org</em:id>
+ <em:version>28at</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>7</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:requires>
+ <Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>72</em:maxVersion>
+ </Description>
+ </em:requires>
+
+ <em:name>Bug 335238</em:name>
+ <em:updateURL>http://localhost:4444/1?id=%ITEM_ID%&amp;version=%ITEM_VERSION%&amp;maxAppVersion=%ITEM_MAXAPPVERSION%&amp;status=%ITEM_STATUS%&amp;appId=%APP_ID%&amp;appVersion=%APP_VERSION%&amp;appOs=%APP_OS%&amp;appAbi=%APP_ABI%&amp;locale=%APP_LOCALE%&amp;reqVersion=%REQ_VERSION%</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug335238_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug335238_3/install.rdf
new file mode 100644
index 000000000..d44448208
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug335238_3/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug335238_3@tests.mozilla.org</em:id>
+ <em:version>58</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:requires>
+ <Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>72</em:maxVersion>
+ </Description>
+ </em:requires>
+
+ <em:name>Bug 335238</em:name>
+ <em:updateURL>http://localhost:4444/2?id=%ITEM_ID%&amp;version=%ITEM_VERSION%&amp;maxAppVersion=%ITEM_MAXAPPVERSION%&amp;status=%ITEM_STATUS%&amp;appId=%APP_ID%&amp;appVersion=%APP_VERSION%&amp;appOs=%APP_OS%&amp;appAbi=%APP_ABI%&amp;locale=%APP_LOCALE%&amp;reqVersion=%REQ_VERSION%</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug335238_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug335238_4/install.rdf
new file mode 100644
index 000000000..6ec052d36
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug335238_4/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug335238_4@tests.mozilla.org</em:id>
+ <em:version>4</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2+</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:requires>
+ <Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>72</em:maxVersion>
+ </Description>
+ </em:requires>
+
+ <em:name>Bug 335238</em:name>
+ <em:updateURL>http://localhost:4444/3?id=%ITEM_ID%&amp;version=%ITEM_VERSION%&amp;maxAppVersion=%ITEM_MAXAPPVERSION%&amp;status=%ITEM_STATUS%&amp;appId=%APP_ID%&amp;appVersion=%APP_VERSION%&amp;appOs=%APP_OS%&amp;appAbi=%APP_ABI%&amp;locale=%APP_LOCALE%&amp;reqVersion=%REQ_VERSION%</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug371495/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug371495/install.rdf
new file mode 100644
index 000000000..c60caf594
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug371495/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<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>bug371495@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test theme</em:name>
+ <em:type>4</em:type>
+ <em:internalName>test/1.0</em:internalName>
+ <em:optionsURL>chrome://foo/content/bar.xul</em:optionsURL>
+ <em:aboutURL>chrome://foo/content/bar.xul</em:aboutURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug394300_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug394300_1/install.rdf
new file mode 100644
index 000000000..2e5ace760
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug394300_1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>bug394300_1@tests.mozilla.org</em:id>
+ <em:version>5</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 394300 Test 1</em:name>
+ <em:updateURL>http://localhost:4444/test_bug394300.rdf</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug394300_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug394300_2/install.rdf
new file mode 100644
index 000000000..ae54424d1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug394300_2/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>bug394300_2@tests.mozilla.org</em:id>
+ <em:version>5</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:name>Bug 394300 Test 2</em:name>
+ <em:updateURL>http://localhost:4444/test_bug394300.rdf</em:updateURL>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf
new file mode 100644
index 000000000..cfcfd406f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug397778/install.rdf
@@ -0,0 +1,78 @@
+<?xml version="1.0"?>
+
+<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>bug397778@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:localized>
+ <Description em:locale="fr">
+ <em:name>fr Name</em:name>
+ <em:description>fr Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="de-DE">
+ <em:name>de-DE Name</em:name>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="ES-es">
+ <em:name>es-ES Name</em:name>
+ <em:description>es-ES Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="zh-TW">
+ <em:name>zh-TW Name</em:name>
+ <em:description>zh-TW Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="zh-CN">
+ <em:name>zh-CN Name</em:name>
+ <em:description>zh-CN Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="en-GB">
+ <em:name>en-GB Name</em:name>
+ <em:description>en-GB Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="en">
+ <em:name>en Name</em:name>
+ <em:description>en Description</em:description>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="en-CA">
+ <em:name>en-CA Name</em:name>
+ <em:description>en-CA Description</em:description>
+ </Description>
+ </em:localized>
+
+ <!-- Front End MetaData -->
+ <em:name>Fallback Name</em:name>
+ <em:description>Fallback Description</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug425657/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug425657/install.rdf
new file mode 100644
index 000000000..e4e1b339b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug425657/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug425657@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Deutsches Wörterbuch</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug470377_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug470377_1/install.rdf
new file mode 100644
index 000000000..5397e8a87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug470377_1/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug470377_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug470377_2/install.rdf
new file mode 100644
index 000000000..b1dde7f7a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug470377_2/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_2@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug470377_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug470377_3/install.rdf
new file mode 100644
index 000000000..ae483434a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug470377_3/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_3@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug470377_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug470377_4/install.rdf
new file mode 100644
index 000000000..97abacc5e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug470377_4/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_4@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug470377_5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug470377_5/install.rdf
new file mode 100644
index 000000000..bff1104a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug470377_5/install.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_5@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug521905/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug521905/install.rdf
new file mode 100644
index 000000000..444bdc556
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug521905/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>bug521905@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Bug 521905</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug567173/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug567173/install.rdf
new file mode 100644
index 000000000..f97bd1302
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug567173/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>bug567173</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bug 567173</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug567184/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bug567184/bootstrap.js
new file mode 100644
index 000000000..09c083532
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug567184/bootstrap.js
@@ -0,0 +1,7 @@
+function install(data, reason) { }
+
+function startup(data, reason) { }
+
+function shutdown(data, reason) { }
+
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug567184/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug567184/install.rdf
new file mode 100644
index 000000000..1e13ceb87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug567184/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bug567184@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Bug 567184 Test</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>undefined</em:minVersion>
+ <em:maxVersion>undefined</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/install.rdf
new file mode 100644
index 000000000..83220ce06
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Bug 587088 Test</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile
new file mode 100644
index 000000000..d2277257f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile
@@ -0,0 +1 @@
+Contents of add-on version 1
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile1 b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile1
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_1/testfile1
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/install.rdf
new file mode 100644
index 000000000..ba23ab802
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Bug 587088 Test</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile
new file mode 100644
index 000000000..07afddfa7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile
@@ -0,0 +1 @@
+Contents of add-on version 2
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile2 b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile2
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug587088_2/testfile2
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug594058/directory/file1 b/toolkit/mozapps/extensions/test/addons/test_bug594058/directory/file1
new file mode 100644
index 000000000..e69de29bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug594058/directory/file1
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf
new file mode 100644
index 000000000..682831949
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug594058/install.rdf
@@ -0,0 +1,21 @@
+<?xml version="1.0"?>
+
+<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>bug594058@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>bug 594058</em:name>
+ <em:description>stat-based invalidation</em:description>
+ <em:unpack>true</em:unpack>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug595573/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug595573/install.rdf
new file mode 100644
index 000000000..36c03fd00
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug595573/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is compatible with the XPCShell test suite -->
+<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>{2f69dacd-03df-4150-a9f1-e8a7b2748829}</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug655254/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug655254/install.rdf
new file mode 100644
index 000000000..a3fa0d707
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug655254/install.rdf
@@ -0,0 +1,18 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Test 1</em:name>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug655254_2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bug655254_2/bootstrap.js
new file mode 100644
index 000000000..b79648e89
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug655254_2/bootstrap.js
@@ -0,0 +1,9 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 1);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 0);
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug655254_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug655254_2/install.rdf
new file mode 100644
index 000000000..71827885f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug655254_2/install.rdf
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:name>Test 2</em:name>
+ <em:bootstrap>true</em:bootstrap>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug659772/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug659772/install.rdf
new file mode 100644
index 000000000..3b34c63d3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug659772/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is compatible with the XPCShell test suite -->
+<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>addon3@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug675371/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_bug675371/chrome.manifest
new file mode 100644
index 000000000..17d5c99ec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug675371/chrome.manifest
@@ -0,0 +1 @@
+content bug675371 .
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug675371/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug675371/install.rdf
new file mode 100644
index 000000000..ca2881e5a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug675371/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bug675371@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Bug 675371 Test</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug675371/test.js b/toolkit/mozapps/extensions/test/addons/test_bug675371/test.js
new file mode 100644
index 000000000..b942a8064
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug675371/test.js
@@ -0,0 +1 @@
+var active = true;
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug740612_1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bug740612_1/bootstrap.js
new file mode 100644
index 000000000..6703e7f7d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug740612_1/bootstrap.js
@@ -0,0 +1 @@
+const APP_STARTUP = 1;
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug740612_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug740612_1/install.rdf
new file mode 100644
index 000000000..b2316273f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug740612_1/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bug740612_1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug740612_2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_bug740612_2/bootstrap.js
new file mode 100644
index 000000000..2ad481453
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug740612_2/bootstrap.js
@@ -0,0 +1,23 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const VERSION = "1.0";
+
+function install(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 0);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug740612_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug740612_2/install.rdf
new file mode 100644
index 000000000..ff4d613ef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug740612_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bug740612_2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 2</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_bug757663/install.rdf b/toolkit/mozapps/extensions/test/addons/test_bug757663/install.rdf
new file mode 100644
index 000000000..be8d85b1b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_bug757663/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>bug757663@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_cacheflush1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_cacheflush1/install.rdf
new file mode 100644
index 000000000..5e64b65c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_cacheflush1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>File Pointer Test</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_cacheflush2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_cacheflush2/install.rdf
new file mode 100644
index 000000000..7728002ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_cacheflush2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>File Pointer Test</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/chrome.manifest
new file mode 100644
index 000000000..4d63b6b06
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/chrome.manifest
@@ -0,0 +1,6 @@
+content test-addon-1 chrome/content
+# comment!
+ locale test-addon-1 en-US locale/en-US
+ # commentaire!
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/install.rdf
new file mode 100644
index 000000000..486be8670
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_1/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/chrome.manifest
new file mode 100644
index 000000000..3b0195077
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/chrome.manifest
@@ -0,0 +1,7 @@
+content test-addon-1 chrome/content
+
+ locale test-addon-1 en-US locale/en-US
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
+binary-component components/something.so
+manifest thisdoesntexist.manifest
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/install.rdf
new file mode 100644
index 000000000..9a9ee4823
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 2</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/chrome.manifest
new file mode 100644
index 000000000..73190ed8f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/chrome.manifest
@@ -0,0 +1,9 @@
+content test-addon-1 chrome/content
+
+ locale test-addon-1 en-US locale/en-US
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
+
+ binary-component components/something.so
+
+ manifest jar:inner.jar!/nested.manifest
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/inner.jar b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/inner.jar
new file mode 100644
index 000000000..b4a40052f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/inner.jar
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/install.rdf
new file mode 100644
index 000000000..3a4a709e0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_3/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 3</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/chrome.manifest
new file mode 100644
index 000000000..60d4f01f0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/chrome.manifest
@@ -0,0 +1,6 @@
+content test-addon-1 chrome/content
+
+ locale test-addon-1 en-US locale/en-US
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
+ manifest components/components.manifest
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/components.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/components.manifest
new file mode 100644
index 000000000..1e0aea440
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/components.manifest
@@ -0,0 +1,2 @@
+binary-component mycomponent.dll
+manifest other/something.manifest
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/other/something.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/other/something.manifest
new file mode 100644
index 000000000..73d58dd66
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/components/other/something.manifest
@@ -0,0 +1 @@
+binary-component thermalnuclearwar.dll
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/install.rdf
new file mode 100644
index 000000000..463e3f27e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_4/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon4@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 4</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/chrome.manifest
new file mode 100644
index 000000000..b0aa32adc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/chrome.manifest
@@ -0,0 +1,7 @@
+content test-addon-1 chrome/content
+
+ locale test-addon-1 en-US locale/en-US
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
+
+ binary-component components/something.so
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/install.rdf
new file mode 100644
index 000000000..7836bced8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_5/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon5@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 5</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>false</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/chrome.manifest
new file mode 100644
index 000000000..4ebb75c30
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/chrome.manifest
@@ -0,0 +1 @@
+resource test-addon-1 .
diff --git a/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/install.rdf
new file mode 100644
index 000000000..5d94de0ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_chromemanifest_6/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon6@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 6</em:name>
+ <em:description>Test Description</em:description>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_data_directory/install.rdf b/toolkit/mozapps/extensions/test/addons/test_data_directory/install.rdf
new file mode 100644
index 000000000..aebfe3b68
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_data_directory/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>datadirectory1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Data Directory 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_1/install.rdf
new file mode 100644
index 000000000..e1f2b5173
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_1/install.rdf
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+
+<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>test_db_sanity_1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+ <em:creator>Keyboard Cat</em:creator>
+ <em:homepageURL>http://mozilla.org/</em:homepageURL>
+
+ <em:contributor>Keyboard Cat 2</em:contributor>
+ <em:translator>Keyboard Cat 3</em:translator>
+
+ <em:localized>
+ <Description>
+ <em:locale>en-1</em:locale>
+ <em:name>Test 1 (en-1)</em:name>
+ <em:description>Test Description (en-1)</em:description>
+ <em:creator>Keyboard Cat (en-1)</em:creator>
+ <em:homepageURL>http://mozilla.org/en-1/</em:homepageURL>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description>
+ <em:locale>en-2</em:locale>
+ <em:name>Test 1 (en-2)</em:name>
+ <em:description>Test Description (en-2)</em:description>
+ <em:creator>Keyboard Cat (en-2)</em:creator>
+ <em:homepageURL>http://mozilla.org/en-2/</em:homepageURL>
+ </Description>
+ </em:localized>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>keyboard-cats-awesome-browser@keyboard.cat</em:id>
+ <em:minVersion>3.1415</em:minVersion>
+ <em:maxVersion>3.1415</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetPlatform>XPCShell_noarch-spidermonkey</em:targetPlatform>
+ <em:targetPlatform>WINNT_x86</em:targetPlatform>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_2/install.rdf
new file mode 100644
index 000000000..da9b067ab
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_db_sanity_1_2/install.rdf
@@ -0,0 +1,59 @@
+<?xml version="1.0"?>
+
+<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>test_db_sanity_1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:name>Test 1</em:name>
+ <em:description>Test Description!!!</em:description>
+ <em:creator>Keyboard Cat</em:creator>
+ <em:homepageURL>http://mozilla.org/</em:homepageURL>
+
+ <em:contributor>Keyboard Cat 2</em:contributor>
+ <em:translator>Keyboard Cat 3</em:translator>
+ <em:translator>Keyboard Cat 4</em:translator>
+
+ <em:localized>
+ <Description>
+ <em:locale>en-1</em:locale>
+ <em:name>Test 1 (en-1)</em:name>
+ <em:description>Test Description (en-1)</em:description>
+ <em:creator>Keyboard Cat (en-1)</em:creator>
+ <em:homepageURL>http://mozilla.org/en-1/</em:homepageURL>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description>
+ <em:locale>en-3</em:locale>
+ <em:name>Test 1 (en-3)</em:name>
+ <em:description>Test Description (en-3)</em:description>
+ <em:creator>Keyboard Cat (en-3)</em:creator>
+ <em:homepageURL>http://mozilla.org/en-3/</em:homepageURL>
+ </Description>
+ </em:localized>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>keyboard-cats-awesome-browser-3000@keyboard.cat</em:id>
+ <em:minVersion>3.1415</em:minVersion>
+ <em:maxVersion>3.1415</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetPlatform>XPCShell_noarch-spidermonkey</em:targetPlatform>
+ <em:targetPlatform>WINNT_i386</em:targetPlatform>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/bootstrap.js
new file mode 100644
index 000000000..c5a80c7b9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/bootstrap.js
@@ -0,0 +1,10 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/install.rdf
new file mode 100644
index 000000000..3ebbe16b0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_v2/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<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>test_delay_update_complete@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Complete</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_webextension_v2/manifest.json b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_webextension_v2/manifest.json
new file mode 100644
index 000000000..1a98f4660
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_complete_webextension_v2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Delay Upgrade",
+ "version": "2.0",
+ "applications": {
+ "gecko": {
+ "id": "test_delay_update_complete_webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/bootstrap.js
new file mode 100644
index 000000000..dea028488
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/bootstrap.js
@@ -0,0 +1,10 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_defer@tests.mozilla.org";
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/install.rdf
new file mode 100644
index 000000000..75c7666bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_v2/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<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>test_delay_update_defer@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Defer</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_webextension_v2/manifest.json b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_webextension_v2/manifest.json
new file mode 100644
index 000000000..9561979dc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_defer_webextension_v2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Delay Upgrade",
+ "version": "2.0",
+ "applications": {
+ "gecko": {
+ "id": "test_delay_update_defer_webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/bootstrap.js
new file mode 100644
index 000000000..fb8fc9540
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/bootstrap.js
@@ -0,0 +1,8 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+function install(data, reason) {}
+
+function startup(data, reason) {}
+
+function shutdown(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/install.rdf
new file mode 100644
index 000000000..aec4b202e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_v2/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>test_delay_update_ignore@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Delay Update Ignore</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+ <em:updateURL>http://localhost:4444/data/test_delay_updates_ignore.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
diff --git a/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_webextension_v2/manifest.json b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_webextension_v2/manifest.json
new file mode 100644
index 000000000..d9200ec11
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_delay_update_ignore_webextension_v2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "manifest_version": 2,
+ "name": "Delay Upgrade",
+ "version": "2.0",
+ "applications": {
+ "gecko": {
+ "id": "test_delay_update_ignore_webext@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_dictionary/chrome.manifest
new file mode 100644
index 000000000..c945c928c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary/chrome.manifest
@@ -0,0 +1 @@
+content dict ./
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary/dictionaries/ab-CD.dic b/toolkit/mozapps/extensions/test/addons/test_dictionary/dictionaries/ab-CD.dic
new file mode 100644
index 000000000..3feac546d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary/dictionaries/ab-CD.dic
@@ -0,0 +1,2 @@
+1
+test1
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary/install.rdf b/toolkit/mozapps/extensions/test/addons/test_dictionary/install.rdf
new file mode 100644
index 000000000..9e66ab237
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<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>ab-CD@dictionaries.addons.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>64</em:type>
+ <em:unpack>true</em:unpack>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Dictionary</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary_2/dictionaries/ab-CD.dic b/toolkit/mozapps/extensions/test/addons/test_dictionary_2/dictionaries/ab-CD.dic
new file mode 100644
index 000000000..b35b9c1a6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary_2/dictionaries/ab-CD.dic
@@ -0,0 +1,2 @@
+1
+test2
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_dictionary_2/install.rdf
new file mode 100644
index 000000000..a74a114fd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>ab-CD@dictionaries.addons.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:unpack>true</em:unpack>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Dictionary</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary_3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_dictionary_3/install.rdf
new file mode 100644
index 000000000..c056e87ff
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary_3/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<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>ab-CD@dictionaries.addons.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:type>64</em:type>
+ <em:unpack>true</em:unpack>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Dictionary</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary_4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_dictionary_4/install.rdf
new file mode 100644
index 000000000..7470284ba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary_4/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>ef@dictionaries.addons.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:unpack>true</em:unpack>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Dictionary ef</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_dictionary_5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_dictionary_5/install.rdf
new file mode 100644
index 000000000..11eba90d7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_dictionary_5/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<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>gh@dictionaries.addons.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:type>64</em:type>
+ <em:unpack>true</em:unpack>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Dictionary gh</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_distribution1_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_distribution1_2/install.rdf
new file mode 100644
index 000000000..8bd5966c9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_distribution1_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Distributed add-ons test</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_experiment1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_experiment1/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_experiment1/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf
new file mode 100644
index 000000000..414a36b30
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_experiment1/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<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>experiment1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>128</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Experiment 1</em:name>
+ <em:description>Test Description</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_filepointer/install.rdf b/toolkit/mozapps/extensions/test/addons/test_filepointer/install.rdf
new file mode 100644
index 000000000..5e64b65c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_filepointer/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>File Pointer Test</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png b/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png
new file mode 100644
index 000000000..40765b0e2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_getresource/icon.png
@@ -0,0 +1 @@
+Dummy icon file \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf b/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf
new file mode 100644
index 000000000..8d2740dbb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_getresource/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+ <em:bootstrap>true</em:bootstrap>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt b/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt
new file mode 100644
index 000000000..a28d18162
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_getresource/subdir/subfile.txt
@@ -0,0 +1 @@
+Dummy file in subdirectory \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf
new file mode 100644
index 000000000..7fcc1a09e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_hotfix_1/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>hotfix@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf
new file mode 100644
index 000000000..fd843dbe9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_hotfix_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>hotfix@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install1/icon.png b/toolkit/mozapps/extensions/test/addons/test_install1/icon.png
new file mode 100644
index 000000000..41409edfe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install1/icon.png
@@ -0,0 +1 @@
+Fake icon image
diff --git a/toolkit/mozapps/extensions/test/addons/test_install1/icon64.png b/toolkit/mozapps/extensions/test/addons/test_install1/icon64.png
new file mode 100644
index 000000000..41409edfe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install1/icon64.png
@@ -0,0 +1 @@
+Fake icon image
diff --git a/toolkit/mozapps/extensions/test/addons/test_install1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install1/install.rdf
new file mode 100644
index 000000000..efe3f18ae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install1/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is compatible with the XPCShell test suite -->
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install2_1/icon.png b/toolkit/mozapps/extensions/test/addons/test_install2_1/icon.png
new file mode 100644
index 000000000..41409edfe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install2_1/icon.png
@@ -0,0 +1 @@
+Fake icon image
diff --git a/toolkit/mozapps/extensions/test/addons/test_install2_1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install2_1/install.rdf
new file mode 100644
index 000000000..116eb7069
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install2_1/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is compatible with the XPCShell test suite -->
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Real Test 2</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install2_2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install2_2/install.rdf
new file mode 100644
index 000000000..7197ea1fb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install2_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is compatible with the XPCShell test suite -->
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>3.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Real Test 3</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install3/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install3/install.rdf
new file mode 100644
index 000000000..8e72017ad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install3/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is incompatible with the XPCShell test suite until
+ a compatibility update check is performed -->
+<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>addon3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Real Test 4</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:updateURL>http://localhost:4444/data/test_install.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/addon4.xpi b/toolkit/mozapps/extensions/test/addons/test_install4/addon4.xpi
new file mode 100644
index 000000000..e57a4f5b6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/addon4.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/addon5.jar b/toolkit/mozapps/extensions/test/addons/test_install4/addon5.jar
new file mode 100644
index 000000000..93fbfbe6e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/addon5.jar
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/addon6.xpi b/toolkit/mozapps/extensions/test/addons/test_install4/addon6.xpi
new file mode 100644
index 000000000..3613dab04
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/addon6.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/addon7.jar b/toolkit/mozapps/extensions/test/addons/test_install4/addon7.jar
new file mode 100644
index 000000000..1af178887
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/addon7.jar
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.jar b/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.jar
new file mode 100644
index 000000000..33695b99f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.jar
@@ -0,0 +1 @@
+This is corrupt
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.xpi b/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.xpi
new file mode 100644
index 000000000..33695b99f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/badaddon.xpi
@@ -0,0 +1 @@
+This is corrupt
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/icon.png b/toolkit/mozapps/extensions/test/addons/test_install4/icon.png
new file mode 100644
index 000000000..57f2c2eb6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/icon.png
@@ -0,0 +1 @@
+This is ignored
diff --git a/toolkit/mozapps/extensions/test/addons/test_install4/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install4/install.rdf
new file mode 100644
index 000000000..5e99ae29a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install4/install.rdf
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<!-- A multi-package XPI -->
+<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:type>32</em:type>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install5/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_install5/chrome.manifest
new file mode 100644
index 000000000..703adf2a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install5/chrome.manifest
@@ -0,0 +1 @@
+binary-component components/mycomponent.so
diff --git a/toolkit/mozapps/extensions/test/addons/test_install5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install5/install.rdf
new file mode 100644
index 000000000..1f96e4b49
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install5/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<!-- An extension that is incompatible with the XPCShell test suite and
+ has binary components, so won't be compatible-by-default. -->
+<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>addon5@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Real Test 5</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install6/install.rdf
new file mode 100644
index 000000000..b1f97c1fd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install6/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<!-- An extension that has a compatibility override making it incompatible. -->
+<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>addon6@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon Test 6</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi b/toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi
new file mode 100644
index 000000000..9c283d1d4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon1.xpi
@@ -0,0 +1 @@
+This isn't a valid zip file. \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi b/toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi
new file mode 100644
index 000000000..9c283d1d4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/addon2.xpi
@@ -0,0 +1 @@
+This isn't a valid zip file. \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/addons/test_install7/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install7/install.rdf
new file mode 100644
index 000000000..5e99ae29a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install7/install.rdf
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<!-- A multi-package XPI -->
+<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:type>32</em:type>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_install8/install.rdf b/toolkit/mozapps/extensions/test/addons/test_install8/install.rdf
new file mode 100644
index 000000000..5e99ae29a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_install8/install.rdf
@@ -0,0 +1,10 @@
+<?xml version="1.0"?>
+
+<!-- A multi-package XPI -->
+<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:type>32</em:type>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js
new file mode 100644
index 000000000..2449baeb8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/bootstrap.js
@@ -0,0 +1,17 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function install(data, reason) {
+ Services.prefs.setIntPref("jetpacktest.installed_version", 1);
+}
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("jetpacktest.active_version", 1);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("jetpacktest.active_version", 0);
+}
+
+function uninstall(data, reason) {
+ Services.prefs.setIntPref("jetpacktest.installed_version", 0);
+}
diff --git a/toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json b/toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json
new file mode 100644
index 000000000..9e26dfeeb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/harness-options.json
@@ -0,0 +1 @@
+{} \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf b/toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf
new file mode 100644
index 000000000..e88794a60
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_jetpack/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>jetpack@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test jetpack</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_langpack/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_langpack/chrome.manifest
new file mode 100644
index 000000000..16fe819a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_langpack/chrome.manifest
@@ -0,0 +1 @@
+locale test-langpack x-testing locale/x-testing
diff --git a/toolkit/mozapps/extensions/test/addons/test_langpack/install.rdf b/toolkit/mozapps/extensions/test/addons/test_langpack/install.rdf
new file mode 100644
index 000000000..056f6dff5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_langpack/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>langpack-x-testing@tests.mozilla.org</em:id>
+ <em:type>8</em:type>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Language Pack x-testing</em:name>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf b/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf
new file mode 100644
index 000000000..d8d027b93
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_locale/install.rdf
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:localized>
+ <Description em:locale="fr-FR">
+ <em:locale/> <!-- Should be ignored and not fail -->
+ <em:name>fr-FR Name</em:name>
+ <em:description>fr-FR Description</em:description>
+ <em:contributor>Fr Contributor 1</em:contributor>
+ <em:contributor>Fr Contributor 2</em:contributor>
+ <em:contributor>Fr Contributor 3</em:contributor>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="de-DE">
+ <em:name>de-DE Name</em:name>
+ </Description>
+ </em:localized>
+
+ <em:localized>
+ <Description em:locale="es-ES">
+ <em:name>es-ES Name</em:name>
+ <em:description>es-ES Description</em:description>
+ </Description>
+ </em:localized>
+
+ <!-- Subsequent definitions for the same locale should be ignored -->
+ <em:localized>
+ <Description em:locale="fr-FR">
+ <em:name>Repeated locale</em:name>
+ </Description>
+ </em:localized>
+
+ <!-- Properties with no listed locale should be ignored -->
+ <em:localized>
+ <Description>
+ <em:name>Missing locale</em:name>
+ </Description>
+ </em:localized>
+
+ <!-- Front End MetaData -->
+ <em:name>Fallback Name</em:name>
+ <em:description>Fallback Description</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_locked2_5/install.rdf b/toolkit/mozapps/extensions/test/addons/test_locked2_5/install.rdf
new file mode 100644
index 000000000..09655c2a6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_locked2_5/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon5@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 5</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_locked2_6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_locked2_6/install.rdf
new file mode 100644
index 000000000..75f110d2a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_locked2_6/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon6@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 6</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate4_6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate4_6/install.rdf
new file mode 100644
index 000000000..5924982f7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate4_6/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon6@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 6</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate4_7/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate4_7/install.rdf
new file mode 100644
index 000000000..072751cf2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate4_7/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon7@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 7</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate6/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate6/install.rdf
new file mode 100644
index 000000000..ff8280ae3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate6/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon6@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 6</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate7/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate7/install.rdf
new file mode 100644
index 000000000..fd1df0e08
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate7/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon7@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 7</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate8/chrome.manifest b/toolkit/mozapps/extensions/test/addons/test_migrate8/chrome.manifest
new file mode 100644
index 000000000..8570bae82
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate8/chrome.manifest
@@ -0,0 +1,6 @@
+content test-addon-1 chrome/content
+
+ locale test-addon-1 en-US locale/en-US
+ locale test-addon-1 fr-FR locale/fr-FR
+overlay chrome://browser/content/browser.xul chrome://test-addon-1/content/overlay.xul
+binary-component components/something.so
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate8/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate8/install.rdf
new file mode 100644
index 000000000..61ed24763
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate8/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>addon8@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 8</em:name>
+ <em:description>Test Description</em:description>
+ <em:unpack>true</em:unpack>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_migrate9/install.rdf b/toolkit/mozapps/extensions/test/addons/test_migrate9/install.rdf
new file mode 100644
index 000000000..116dd0176
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_migrate9/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<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>addon9@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:internalName>theme1/1.0</em:internalName>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Theme 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:skinnable>true</em:skinnable>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_symbol/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_symbol/bootstrap.js
new file mode 100644
index 000000000..4eb8b4875
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_symbol/bootstrap.js
@@ -0,0 +1,62 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const PASS_PREF = "symboltest.instanceid.pass";
+const FAIL_BOGUS_PREF = "symboltest.instanceid.fail_bogus";
+const FAIL_ID_PREF = "symboltest.instanceid.fail_bogus";
+const ADDON_ID = "test_symbol@tests.mozilla.org";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ Services.prefs.setBoolPref(PASS_PREF, false);
+ Services.prefs.setBoolPref(FAIL_BOGUS_PREF, false);
+ Services.prefs.setBoolPref(FAIL_ID_PREF, false);
+
+ // test with the correct symbol
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.getAddonByInstanceID(data.instanceID)
+ .then(addon => {
+ if (addon.id == ADDON_ID) {
+ Services.prefs.setBoolPref(PASS_PREF, true);
+ }
+ }).catch(err => {
+ throw Error("no addon found for symbol");
+ });
+
+ }
+
+ // test with a totally bogus symbol
+ AddonManager.getAddonByInstanceID(Symbol("bad symbol"))
+ .then(addon => {
+ // there is no symbol by this name, so null should be returned
+ if (addon == null) {
+ Services.prefs.setBoolPref(FAIL_BOGUS_PREF, true);
+ } else {
+ throw Error("bad symbol should not match:", addon);
+ }
+ }).catch(err => {
+ throw Error("promise should not have rejected: " + err);
+ });
+
+ // try to make a matching symbol - this should fail because it's not a
+ // reference to the same symbol stored inside the addons manager.
+ AddonManager.getAddonByInstanceID(Symbol(ADDON_ID))
+ .then(addon => {
+ // there is no symbol by this name, so null should be returned
+ if (addon == null) {
+ Services.prefs.setBoolPref(FAIL_ID_PREF, true);
+ } else {
+ throw Error("bad symbol should not match:", addon);
+ }
+ }).catch(err => {
+ throw Error("promise should not have rejected: " + err);
+ });
+
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_symbol/install.rdf b/toolkit/mozapps/extensions/test/addons/test_symbol/install.rdf
new file mode 100644
index 000000000..adccda552
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_symbol/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>test_symbol@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Symbol</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_theme/install.rdf b/toolkit/mozapps/extensions/test/addons/test_theme/install.rdf
new file mode 100644
index 000000000..e1a37d0a4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_theme/install.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0"?>
+
+<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>theme1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:internalName>theme1/1.0</em:internalName>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Theme 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:skinnable>true</em:skinnable>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_theme/preview.png b/toolkit/mozapps/extensions/test/addons/test_theme/preview.png
new file mode 100644
index 000000000..321ce47cf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_theme/preview.png
@@ -0,0 +1 @@
+Fake preview image
diff --git a/toolkit/mozapps/extensions/test/addons/test_undoincompatible/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_undoincompatible/install.rdf b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/install.rdf
new file mode 100644
index 000000000..b038ebc51
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undoincompatible/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>incompatible@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Incompatible Addon</em:name>
+ <em:description>I am incompatible</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_undouninstall1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/addons/test_undouninstall1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/install.rdf
new file mode 100644
index 000000000..4178fe929
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_undouninstall1/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>undouninstall1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_update/install.rdf b/toolkit/mozapps/extensions/test/addons/test_update/install.rdf
new file mode 100644
index 000000000..801a35a8f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_update12/install.rdf b/toolkit/mozapps/extensions/test/addons/test_update12/install.rdf
new file mode 100644
index 000000000..3589cb55c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update12/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon12@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 12</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_update8/install.rdf b/toolkit/mozapps/extensions/test/addons/test_update8/install.rdf
new file mode 100644
index 000000000..43e31af42
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update8/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon8@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test 8</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_update_multi1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_update_multi1/bootstrap.js
new file mode 100644
index 000000000..24c778c09
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update_multi1/bootstrap.js
@@ -0,0 +1,5 @@
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_update_multi1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_update_multi1/install.rdf
new file mode 100644
index 000000000..9f955562c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update_multi1/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<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>updatemulti@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:updateURL>http://localhost:4444/data/test_update_multi.rdf</em:updateURL>
+ <em:bootstrap>true</em:bootstrap>
+ <em:name>Test Addon 1</em:name>
+<em:targetApplication><Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+</Description></em:targetApplication>
+</Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_update_multi2/addon.xpi b/toolkit/mozapps/extensions/test/addons/test_update_multi2/addon.xpi
new file mode 100644
index 000000000..febff06fa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update_multi2/addon.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/addons/test_update_multi2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_update_multi2/install.rdf
new file mode 100644
index 000000000..44bdb05ba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_update_multi2/install.rdf
@@ -0,0 +1,9 @@
+<?xml version="1.0"?>
+<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>updatemulti@tests.mozilla.org</em:id>
+ <em:type>32</em:type>
+ <em:version>2.0</em:version>
+</Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_updateid1/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_updateid1/bootstrap.js
new file mode 100644
index 000000000..24c778c09
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_updateid1/bootstrap.js
@@ -0,0 +1,5 @@
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_updateid1/install.rdf b/toolkit/mozapps/extensions/test/addons/test_updateid1/install.rdf
new file mode 100644
index 000000000..803b64e5f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_updateid1/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<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>addon1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:updateURL>http://localhost:4444/data/test_updateid.rdf</em:updateURL>
+ <em:bootstrap>true</em:bootstrap>
+ <em:name>Test Addon 1</em:name>
+<em:targetApplication><Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+</Description></em:targetApplication>
+</Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/test_updateid2/bootstrap.js b/toolkit/mozapps/extensions/test/addons/test_updateid2/bootstrap.js
new file mode 100644
index 000000000..24c778c09
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_updateid2/bootstrap.js
@@ -0,0 +1,5 @@
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/addons/test_updateid2/install.rdf b/toolkit/mozapps/extensions/test/addons/test_updateid2/install.rdf
new file mode 100644
index 000000000..041af57f0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/test_updateid2/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+<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>addon1.changed@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:updateURL>http://localhost:4444/data/test_updateid.rdf</em:updateURL>
+ <em:bootstrap>true</em:bootstrap>
+ <em:name>Test Addon 1</em:name>
+<em:targetApplication><Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+</Description></em:targetApplication>
+</Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_1/install.rdf b/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_1/install.rdf
new file mode 100644
index 000000000..76e662977
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_1/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>upgradeable1x2-3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test min 1 max 2 upgrade to 3</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_2/install.rdf b/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_2/install.rdf
new file mode 100644
index 000000000..e57672c42
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/upgradeable1x2-3_2/install.rdf
@@ -0,0 +1,22 @@
+<?xml version="1.0"?>
+
+<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>upgradeable1x2-3@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>3</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Test min 1 max 2 upgrade to 3</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_1/chrome.manifest b/toolkit/mozapps/extensions/test/addons/webextension_1/chrome.manifest
new file mode 100644
index 000000000..16108ce18
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_1/chrome.manifest
@@ -0,0 +1 @@
+content webex ./
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_1/manifest.json b/toolkit/mozapps/extensions/test/addons/webextension_1/manifest.json
new file mode 100644
index 000000000..2ca5f40f7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_1/manifest.json
@@ -0,0 +1,14 @@
+{
+ "name": "Web Extension Name",
+ "version": "1.0",
+ "manifest_version": 2,
+ "applications": {
+ "gecko": {
+ "id": "webextension1@tests.mozilla.org"
+ }
+ },
+ "icons": {
+ "48": "icon48.png",
+ "64": "icon64.png"
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf b/toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf
new file mode 100644
index 000000000..653481ed0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>first-webextension2@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>XPI Add-on 1</em:name>
+ <em:description>XPI Add-on 1 - Description</em:description>
+ <em:creator>XPI Add-on 1 - Creator</em:creator>
+ <em:developer>XPI Add-on 1 - First Developer</em:developer>
+ <em:translator>XPI Add-on 1 - First Translator</em:translator>
+ <em:contributor>XPI Add-on 1 - First Contributor</em:contributor>
+ <em:homepageURL>http://localhost/xpi/1/homepage.html</em:homepageURL>
+ <em:optionsURL>http://localhost/xpi/1/options.html</em:optionsURL>
+ <em:aboutURL>http://localhost/xpi/1/about.html</em:aboutURL>
+ <em:iconURL>http://localhost/xpi/1/icon.png</em:iconURL>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json b/toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json
new file mode 100644
index 000000000..5b470d430
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_2/manifest.json
@@ -0,0 +1,10 @@
+{
+ "name": "Web Extension Name",
+ "version": "1.0",
+ "manifest_version": 2,
+ "applications": {
+ "gecko": {
+ "id": "last-webextension2@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/en/messages.json b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/en/messages.json
new file mode 100644
index 000000000..36868d414
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/en/messages.json
@@ -0,0 +1,10 @@
+{
+ "name": {
+ "message": "foo ☹",
+ "description": "foo"
+ },
+ "desc": {
+ "message": "bar ☹",
+ "description": "bar"
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/fr/messages.json b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/fr/messages.json
new file mode 100644
index 000000000..c3d02ffde
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/_locales/fr/messages.json
@@ -0,0 +1,10 @@
+{
+ "name": {
+ "message": "le foo ☺",
+ "description": "foo"
+ },
+ "desc": {
+ "message": "le bar ☺",
+ "description": "bar"
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/addons/webextension_3/manifest.json b/toolkit/mozapps/extensions/test/addons/webextension_3/manifest.json
new file mode 100644
index 000000000..b6ae6f10f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/addons/webextension_3/manifest.json
@@ -0,0 +1,12 @@
+{
+ "name": "Web Extensiøn __MSG_name__",
+ "description": "Descriptïon __MSG_desc__ of add-on",
+ "version": "1.0",
+ "manifest_version": 2,
+ "default_locale": "en",
+ "applications": {
+ "gecko": {
+ "id": "webextension3@tests.mozilla.org"
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/.eslintrc.js b/toolkit/mozapps/extensions/test/browser/.eslintrc.js
new file mode 100644
index 000000000..2852eb81d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/extensions/test/browser/addon_about.xul b/toolkit/mozapps/extensions/test/browser/addon_about.xul
new file mode 100644
index 000000000..c2b8b935e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addon_about.xul
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="addon-test-about-window">
+ <label value="Oh hai!"/>
+</window>
diff --git a/toolkit/mozapps/extensions/test/browser/addon_prefs.xul b/toolkit/mozapps/extensions/test/browser/addon_prefs.xul
new file mode 100644
index 000000000..85cfe6b2d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addon_prefs.xul
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ id="addon-test-pref-window">
+ <label value="Oh hai!"/>
+</window>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1.xpi
new file mode 100644
index 000000000..d13e5d66e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1/install.rdf
new file mode 100644
index 000000000..5199c2f3f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon1</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10.xpi
new file mode 100644
index 000000000..e1d6ac586
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10/install.rdf
new file mode 100644
index 000000000..c061814b6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_10/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-10@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon10</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2.xpi
new file mode 100644
index 000000000..1380e7b5b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2/install.rdf
new file mode 100644
index 000000000..1be5422b2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_2/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon2</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3.xpi
new file mode 100644
index 000000000..46326b04c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3/install.rdf
new file mode 100644
index 000000000..54887a30e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_3/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-3@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon3</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4.xpi
new file mode 100644
index 000000000..95de79b79
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4/install.rdf
new file mode 100644
index 000000000..79946b0f0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_4/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-4@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>1.0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon4</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5.xpi
new file mode 100644
index 000000000..005d7ef21
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5/install.rdf
new file mode 100644
index 000000000..067af1ba1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_5/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-5@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon5</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6.xpi
new file mode 100644
index 000000000..89f751a03
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6/install.rdf
new file mode 100644
index 000000000..37d7e6a7c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_6/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-6@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon6</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7.xpi
new file mode 100644
index 000000000..77de37c2a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7/install.rdf
new file mode 100644
index 000000000..461facbb2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_7/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-7@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon7</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1.xpi
new file mode 100644
index 000000000..666ae0910
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1/install.rdf
new file mode 100644
index 000000000..2111b9c64
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_8_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-8@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon8</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1.xpi
new file mode 100644
index 000000000..ad9367dc1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1/install.rdf
new file mode 100644
index 000000000..032e9adaf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug557956_9_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug557956-9@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>0.3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Addon9</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1.xpi
new file mode 100644
index 000000000..20ae35539
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1/install.rdf
new file mode 100644
index 000000000..d9f0ca745
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug567127_1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>browser_bug567127 #1</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2.xpi
new file mode 100644
index 000000000..3c99022cf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2/install.rdf
new file mode 100644
index 000000000..8809ffb58
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug567127_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>bug567127_2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>browser_bug567127 #2</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1.xpi
new file mode 100644
index 000000000..c438cbd0b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1/install.rdf
new file mode 100644
index 000000000..c768d7881
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug596336-1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Bootstrap upgrade test</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2.xpi
new file mode 100644
index 000000000..a87191efe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2/install.rdf
new file mode 100644
index 000000000..bed489c3e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_bug596336_2/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>bug596336-1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Bootstrap upgrade test</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1.xpi
new file mode 100644
index 000000000..d8b4d083c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/install.rdf
new file mode 100644
index 000000000..9bcd43e87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop1/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>dragdrop1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Drag Drop test 1</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2.xpi
new file mode 100644
index 000000000..e4441cb19
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/install.rdf
new file mode 100644
index 000000000..4789abfaf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_dragdrop2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>dragdrop2@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Drag Drop test 2</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1.xpi
new file mode 100644
index 000000000..1c2b793f1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1/install.rdf
new file mode 100644
index 000000000..92f20a4ef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_experiment1/install.rdf
@@ -0,0 +1,16 @@
+<?xml version="1.0"?>
+
+<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>test-experiment1@experiments.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>128</em:type>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Experiment 1</em:name>
+ <em:description>Test Description</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1.xpi
new file mode 100644
index 000000000..be453ce9c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/bootstrap.js
new file mode 100644
index 000000000..7871af738
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/bootstrap.js
@@ -0,0 +1,8 @@
+function install (params, aReason) {
+}
+function uninstall (params, aReason) {
+}
+function startup (params, aReason) {
+}
+function shutdown (params, aReason) {
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/chrome.manifest b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/chrome.manifest
new file mode 100644
index 000000000..8884e3974
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/chrome.manifest
@@ -0,0 +1 @@
+locale inlinesettings en-US ./
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/install.rdf
new file mode 100644
index 000000000..18fcec167
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0" ?>
+<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>inlinesettings1@tests.mozilla.org</em:id>
+ <em:name>Inline Settings (Bootstrap)</em:name>
+ <em:version>1</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/options.xul b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/options.xul
new file mode 100644
index 000000000..cd4f72387
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/options.xul
@@ -0,0 +1,23 @@
+<?xml version="1.0" ?>
+
+<!DOCTYPE vbox SYSTEM "chrome://inlinesettings/locale/settings.dtd">
+
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting pref="extensions.inlinesettings1.bool" type="bool" title="Bool" checkboxlabel="&checkbox;"/>
+ <setting pref="extensions.inlinesettings1.boolint" type="boolint" on="1" off="2" title="BoolInt"/>
+ <setting pref="extensions.inlinesettings1.integer" type="integer" title="Integer"/>
+ <setting pref="extensions.inlinesettings1.string" type="string" title="String"/>
+ <setting type="control" title="Menulist">
+ <menulist sizetopopup="always" oncommand="window._testValue = this.value;">
+ <menupopup>
+ <menuitem label="Alpha" value="1" />
+ <menuitem label="Bravo" value="2" />
+ <menuitem label="Charlie" value="3" />
+ </menupopup>
+ </menulist>
+ </setting>
+ <setting pref="extensions.inlinesettings1.color" type="color" title="Color"/>
+ <setting pref="extensions.inlinesettings1.file" type="file" title="File"/>
+ <setting pref="extensions.inlinesettings1.directory" type="directory" title="Directory"/>
+ <setting pref="extensions.inlinesettings1.integer-size" type="integer" title="Integer with size" size="1" />
+</vbox>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/settings.dtd b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/settings.dtd
new file mode 100644
index 000000000..6864a3d5a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1/settings.dtd
@@ -0,0 +1 @@
+<!ENTITY checkbox "Check box label">
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom.xpi
new file mode 100644
index 000000000..6e937c5cc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/binding.xml b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/binding.xml
new file mode 100644
index 000000000..6ac72a03c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/binding.xml
@@ -0,0 +1,19 @@
+<?xml version="1.0"?>
+<bindings xmlns="http://www.mozilla.org/xbl"
+ xmlns:xbl="http://www.mozilla.org/xbl"
+ xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <binding id="custom"
+ extends="chrome://mozapps/content/extensions/setting.xml#setting-base">
+ <content>
+ <xul:vbox>
+ <xul:hbox class="preferences-alignment">
+ <xul:label anonid="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:label anonid="input" value="Woah!"/>
+ </xul:hbox>
+ </content>
+ </binding>
+</bindings>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/bootstrap.js
new file mode 100644
index 000000000..7871af738
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/bootstrap.js
@@ -0,0 +1,8 @@
+function install (params, aReason) {
+}
+function uninstall (params, aReason) {
+}
+function startup (params, aReason) {
+}
+function shutdown (params, aReason) {
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/chrome.manifest b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/chrome.manifest
new file mode 100644
index 000000000..f1eaef1d0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/chrome.manifest
@@ -0,0 +1,2 @@
+content inlinesettings ./ contentaccessible=yes
+locale inlinesettings en-US ./
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/install.rdf
new file mode 100644
index 000000000..a04046a74
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/install.rdf
@@ -0,0 +1,27 @@
+<?xml version="1.0" ?>
+<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>inlinesettings1@tests.mozilla.org</em:id>
+ <em:name>Inline Settings (Bootstrap)</em:name>
+ <em:version>2</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/options.xul b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/options.xul
new file mode 100644
index 000000000..148fb9856
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/options.xul
@@ -0,0 +1,5 @@
+<?xml version="1.0" ?>
+<!DOCTYPE vbox SYSTEM "chrome://inlinesettings/locale/string.dtd">
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting type="custom" title="&custom.title;" style="background-color: blue; display: -moz-grid-line; -moz-binding: url('chrome://inlinesettings/content/binding.xml#custom');"/>
+</vbox>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/string.dtd b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/string.dtd
new file mode 100644
index 000000000..0b2dcc8fe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_custom/string.dtd
@@ -0,0 +1 @@
+<!ENTITY custom.title "Custom">
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info.xpi
new file mode 100644
index 000000000..4c939c05b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/bootstrap.js
new file mode 100644
index 000000000..7871af738
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/bootstrap.js
@@ -0,0 +1,8 @@
+function install (params, aReason) {
+}
+function uninstall (params, aReason) {
+}
+function startup (params, aReason) {
+}
+function shutdown (params, aReason) {
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/install.rdf
new file mode 100644
index 000000000..ba90bd57b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0" ?>
+<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>inlinesettings1@tests.mozilla.org</em:id>
+ <em:name>Inline Settings (Bootstrap)</em:name>
+ <em:version>3</em:version>
+ <em:bootstrap>true</em:bootstrap>
+ <em:optionsType>4</em:optionsType>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/options.xul b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/options.xul
new file mode 100644
index 000000000..095d3bcef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_inlinesettings1_info/options.xul
@@ -0,0 +1,19 @@
+<?xml version="1.0" ?>
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting pref="extensions.inlinesettings1.bool" type="bool" title="Bool" checkboxlabel="Check box label"/>
+ <setting pref="extensions.inlinesettings1.boolint" type="boolint" on="1" off="2" title="BoolInt"/>
+ <setting pref="extensions.inlinesettings1.integer" type="integer" title="Integer"/>
+ <setting pref="extensions.inlinesettings1.string" type="string" title="String"/>
+ <setting type="control" title="Menulist">
+ <menulist sizetopopup="always" oncommand="window._testValue = this.value;">
+ <menupopup>
+ <menuitem label="Alpha" value="1" />
+ <menuitem label="Bravo" value="2" />
+ <menuitem label="Charlie" value="3" />
+ </menupopup>
+ </menulist>
+ </setting>
+ <setting pref="extensions.inlinesettings1.color" type="color" title="Color"/>
+ <setting pref="extensions.inlinesettings1.file" type="file" title="File"/>
+ <setting pref="extensions.inlinesettings1.directory" type="directory" title="Directory"/>
+</vbox>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1.xpi
new file mode 100644
index 000000000..d6df8f9d0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1/install.rdf
new file mode 100644
index 000000000..b11b6d7e4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_1/install.rdf
@@ -0,0 +1,32 @@
+<?xml version="1.0"?>
+
+<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>install1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+ <em:updateURL>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_install.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Install Tests</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi
new file mode 100644
index 000000000..5468e6cb0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2/install.rdf
new file mode 100644
index 000000000..81898b24d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>install1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Install Tests</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl.xpi
new file mode 100644
index 000000000..999535a97
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/install.rdf
new file mode 100644
index 000000000..7ce6cd09a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_installssl/install.rdf
@@ -0,0 +1,30 @@
+<?xml version="1.0"?>
+
+<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>sslinstall@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>SSL Install Tests</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpi
new file mode 100644
index 000000000..efef815aa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_searching/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_searching/bootstrap.js
new file mode 100644
index 000000000..7b86e419a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_searching/bootstrap.js
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
+
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_searching/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_searching/install.rdf
new file mode 100644
index 000000000..02cc935ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_searching/install.rdf
@@ -0,0 +1,25 @@
+<?xml version="1.0"?>
+
+<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>remote1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:type>2</em:type>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>PASS - b - installed</em:name>
+ <em:description>Test sumary - SEARCH SEARCH</em:description>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1.xpi
new file mode 100644
index 000000000..956812aaa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/bootstrap.js
new file mode 100644
index 000000000..ff5b80ae8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/bootstrap.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Services.ppmm.loadProcessScript(
+ "resource://my-addon/frame-script.js", false);
+}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/chrome.manifest b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/chrome.manifest
new file mode 100644
index 000000000..f1a7ccee1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/chrome.manifest
@@ -0,0 +1 @@
+resource my-addon .
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/frame-script.js b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/frame-script.js
new file mode 100644
index 000000000..f4674b840
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/frame-script.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Services.cpmm.sendAsyncMessage("my-addon-1");
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/install.rdf
new file mode 100644
index 000000000..c52307b4a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_1/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>update1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Update Tests</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2.xpi
new file mode 100644
index 000000000..7ef3db940
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/bootstrap.js
new file mode 100644
index 000000000..ff5b80ae8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/bootstrap.js
@@ -0,0 +1,12 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function install(data, reason) {}
+function startup(data, reason) {
+ Components.utils.import("resource://gre/modules/Services.jsm");
+ Services.ppmm.loadProcessScript(
+ "resource://my-addon/frame-script.js", false);
+}
+function shutdown(data, reason) {}
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/chrome.manifest b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/chrome.manifest
new file mode 100644
index 000000000..f1a7ccee1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/chrome.manifest
@@ -0,0 +1 @@
+resource my-addon .
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/frame-script.js b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/frame-script.js
new file mode 100644
index 000000000..e35092a55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/frame-script.js
@@ -0,0 +1,6 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+Services.cpmm.sendAsyncMessage("my-addon-2");
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/install.rdf
new file mode 100644
index 000000000..2fae190b4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_update1_2/install.rdf
@@ -0,0 +1,31 @@
+<?xml version="1.0"?>
+
+<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>update1@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Update Tests</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install.xpi b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install.xpi
new file mode 100644
index 000000000..4fa7b8bfa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/bootstrap.js b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/bootstrap.js
new file mode 100644
index 000000000..bd11077c2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/bootstrap.js
@@ -0,0 +1,9 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("webapitest.active_version", 1);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("webapitest.active_version", 0);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/install.rdf b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/install.rdf
new file mode 100644
index 000000000..2cae124e1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/browser_webapi_install/install.rdf
@@ -0,0 +1,29 @@
+<?xml version="1.0"?>
+
+<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>webapi_install@tests.mozilla.org</em:id>
+ <em:version>1.1</em:version>
+ <em:name>AddonManger web API test</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0.3</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed.xpi b/toolkit/mozapps/extensions/test/browser/addons/options_signed.xpi
new file mode 100644
index 000000000..a063fd1c4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json
new file mode 100644
index 000000000..e808cd5ab
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/manifest.json
@@ -0,0 +1,11 @@
+{
+ "manifest_version": 2,
+
+ "name": "Test options_ui",
+ "description": "Test add-ons manager handling options_ui with no id in manifest.json",
+ "version": "1.2",
+
+ "options_ui": {
+ "page": "options.html"
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html
new file mode 100644
index 000000000..ea804601b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/addons/options_signed/options.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8" />
+ </head>
+ <body>
+ <div id="options-test-panel" />
+ </body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/blockNoPlugins.xml b/toolkit/mozapps/extensions/test/browser/blockNoPlugins.xml
new file mode 100644
index 000000000..e4e191b37
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/blockNoPlugins.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310001">
+ <emItems>
+ </emItems>
+ <pluginItems>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/browser/blockPluginHard.xml b/toolkit/mozapps/extensions/test/browser/blockPluginHard.xml
new file mode 100644
index 000000000..24eb5bc6f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/blockPluginHard.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1336406310000">
+ <emItems>
+ </emItems>
+ <pluginItems>
+ <pluginItem blockID="p9999">
+ <match name="filename" exp="libnptest\.so|nptest\.dll|Test\.plugin" />
+ <versionRange severity="2"></versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/browser/browser-common.ini b/toolkit/mozapps/extensions/test/browser/browser-common.ini
new file mode 100644
index 000000000..eda266e2f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser-common.ini
@@ -0,0 +1,67 @@
+[browser_about.js]
+skip-if = os == 'linux' || os == 'win' # bug 632290
+[browser_bug523784.js]
+[browser_bug557943.js]
+[browser_bug562797.js]
+[browser_bug562854.js]
+[browser_bug562890.js]
+skip-if = os == 'win' && !debug # Disabled on Windows opt/PGO builds due to intermittent failures (bug 1135866)
+[browser_bug562899.js]
+skip-if = buildapp == 'mulet'
+[browser_bug562992.js]
+[browser_bug567127.js]
+[browser_bug567137.js]
+[browser_bug570760.js]
+[browser_bug572561.js]
+[browser_bug573062.js]
+[browser_bug577990.js]
+[browser_bug580298.js]
+[browser_bug581076.js]
+[browser_bug586574.js]
+[browser_bug587970.js]
+[browser_bug591465.js]
+[browser_bug591663.js]
+[browser_bug593535.js]
+skip-if = true # Bug 1093190 - Disabled due to leak
+[browser_bug596336.js]
+[browser_bug608316.js]
+[browser_bug610764.js]
+[browser_bug618502.js]
+[browser_bug679604.js]
+[browser_bug714593.js]
+[browser_bug590347.js]
+[browser_details.js]
+[browser_discovery.js]
+[browser_dragdrop.js]
+skip-if = buildapp == 'mulet'
+[browser_experiments.js]
+[browser_list.js]
+[browser_metadataTimeout.js]
+[browser_searching.js]
+[browser_sorting.js]
+[browser_sorting_plugins.js]
+[browser_plugin_enabled_state_locked.js]
+[browser_uninstalling.js]
+[browser_install.js]
+[browser_recentupdates.js]
+[browser_manualupdates.js]
+[browser_globalwarnings.js]
+[browser_eula.js]
+skip-if = buildapp == 'mulet'
+[browser_updateid.js]
+[browser_purchase.js]
+[browser_openDialog.js]
+tags = openwindow
+skip-if = os == 'win' # Disabled on Windows due to intermittent failures (bug 1135866)
+[browser_types.js]
+[browser_inlinesettings.js]
+[browser_inlinesettings_browser.js]
+[browser_inlinesettings_custom.js]
+[browser_inlinesettings_info.js]
+[browser_tabsettings.js]
+[browser_pluginprefs.js]
+skip-if = buildapp == 'mulet'
+[browser_CTP_plugins.js]
+skip-if = buildapp == 'mulet'
+[browser_webext_options.js]
+tags = webextensions
diff --git a/toolkit/mozapps/extensions/test/browser/browser-window.ini b/toolkit/mozapps/extensions/test/browser/browser-window.ini
new file mode 100644
index 000000000..fcda90fc6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser-window.ini
@@ -0,0 +1,52 @@
+[DEFAULT]
+install-to-subdir = test-window
+support-files =
+ addons/*
+ addon_about.xul
+ addon_prefs.xul
+ cancelCompatCheck.sjs
+ discovery.html
+ discovery_frame.html
+ discovery_install.html
+ head.js
+ signed_hotfix.rdf
+ signed_hotfix.xpi
+ unsigned_hotfix.rdf
+ unsigned_hotfix.xpi
+ more_options.xul
+ options.xul
+ plugin_test.html
+ redirect.sjs
+ releaseNotes.xhtml
+ blockNoPlugins.xml
+ blockPluginHard.xml
+ browser_bug557956.rdf
+ browser_bug557956_8_2.xpi
+ browser_bug557956_9_2.xpi
+ browser_bug557956.xml
+ browser_bug591465.xml
+ browser_bug593535.xml
+ browser_searching.xml
+ browser_searching_empty.xml
+ browser_updatessl.rdf
+ browser_updatessl.rdf^headers^
+ browser_install.rdf
+ browser_install.rdf^headers^
+ browser_install.xml
+ browser_install1_3.xpi
+ browser_eula.xml
+ browser_purchase.xml
+ webapi_addon_listener.html
+ webapi_checkavailable.html
+ webapi_checkchromeframe.xul
+ webapi_checkframed.html
+ webapi_checknavigatedwindow.html
+ !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
+ !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
+
+[include:browser-common.ini]
diff --git a/toolkit/mozapps/extensions/test/browser/browser.ini b/toolkit/mozapps/extensions/test/browser/browser.ini
new file mode 100644
index 000000000..a23841d33
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser.ini
@@ -0,0 +1,75 @@
+[DEFAULT]
+tags = addons
+support-files =
+ addons/*
+ addon_about.xul
+ addon_prefs.xul
+ cancelCompatCheck.sjs
+ discovery.html
+ discovery_frame.html
+ discovery_install.html
+ head.js
+ signed_hotfix.rdf
+ signed_hotfix.xpi
+ unsigned_hotfix.rdf
+ unsigned_hotfix.xpi
+ more_options.xul
+ options.xul
+ plugin_test.html
+ redirect.sjs
+ releaseNotes.xhtml
+ blockNoPlugins.xml
+ blockPluginHard.xml
+ browser_bug557956.rdf
+ browser_bug557956_8_2.xpi
+ browser_bug557956_9_2.xpi
+ browser_bug557956.xml
+ browser_bug591465.xml
+ browser_bug593535.xml
+ browser_searching.xml
+ browser_searching_empty.xml
+ browser_updatessl.rdf
+ browser_updatessl.rdf^headers^
+ browser_install.rdf
+ browser_install.rdf^headers^
+ browser_install.xml
+ browser_install1_3.xpi
+ browser_eula.xml
+ browser_purchase.xml
+ webapi_addon_listener.html
+ webapi_checkavailable.html
+ webapi_checkchromeframe.xul
+ webapi_checkframed.html
+ webapi_checknavigatedwindow.html
+ !/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
+ !/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
+ !/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
+
+[browser_addonrepository_performance.js]
+[browser_bug557956.js]
+[browser_bug616841.js]
+[browser_cancelCompatCheck.js]
+[browser_checkAddonCompatibility.js]
+[browser_gmpProvider.js]
+[browser_hotfix.js]
+# Verifies the old style of signing hotfixes
+skip-if = require_signing
+[browser_installssl.js]
+[browser_newaddon.js]
+[browser_updatessl.js]
+[browser_system_addons_are_e10s.js]
+[browser_task_next_test.js]
+[browser_discovery_install.js]
+[browser_update.js]
+[browser_webapi.js]
+[browser_webapi_access.js]
+[browser_webapi_addon_listener.js]
+[browser_webapi_enable.js]
+[browser_webapi_install.js]
+[browser_webapi_uninstall.js]
+
+[include:browser-common.ini]
diff --git a/toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js b/toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js
new file mode 100644
index 000000000..dd2992f81
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_CTP_plugins.js
@@ -0,0 +1,172 @@
+const gHttpTestRoot = "http://127.0.0.1:8888/" + RELATIVE_DIR + "/";
+
+function updateBlocklist(aCallback) {
+ var blocklistNotifier = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ var observer = function() {
+ Services.obs.removeObserver(observer, "blocklist-updated");
+ SimpleTest.executeSoon(aCallback);
+ };
+ Services.obs.addObserver(observer, "blocklist-updated", false);
+ blocklistNotifier.notify(null);
+}
+
+var _originalBlocklistURL = null;
+function setAndUpdateBlocklist(aURL, aCallback) {
+ if (!_originalBlocklistURL) {
+ _originalBlocklistURL = Services.prefs.getCharPref("extensions.blocklist.url");
+ }
+ Services.prefs.setCharPref("extensions.blocklist.url", aURL);
+ updateBlocklist(aCallback);
+}
+
+function resetBlocklist(aCallback) {
+ Services.prefs.setCharPref("extensions.blocklist.url", _originalBlocklistURL);
+}
+
+add_task(function*() {
+ SpecialPowers.pushPrefEnv({"set": [
+ ["plugins.click_to_play", true],
+ ["extensions.blocklist.suppressUI", true]
+ ]});
+ registerCleanupFunction(function*() {
+ let pluginTag = getTestPluginTag();
+ pluginTag.enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ yield new Promise(resolve => {
+ setAndUpdateBlocklist(gHttpTestRoot + "blockNoPlugins.xml", resolve);
+ });
+ resetBlocklist();
+ });
+
+ let pluginTag = getTestPluginTag();
+ pluginTag.enabledState = Ci.nsIPluginTag.STATE_CLICKTOPLAY;
+ let managerWindow = yield new Promise(resolve => open_manager("addons://list/plugin", resolve));
+
+ let plugins = yield new Promise(resolve => AddonManager.getAddonsByTypes(["plugin"], resolve));
+
+ let testPluginId;
+ for (let plugin of plugins) {
+ if (plugin.name == "Test Plug-in") {
+ testPluginId = plugin.id;
+ break;
+ }
+ }
+ ok(testPluginId, "part2: Test Plug-in should exist");
+
+ let testPlugin = yield new Promise(resolve => AddonManager.getAddonByID(testPluginId, resolve));
+
+ let pluginEl = get_addon_element(managerWindow, testPluginId);
+ pluginEl.parentNode.ensureElementIsVisible(pluginEl);
+ let enableButton = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "enable-btn");
+ is_element_hidden(enableButton, "part3: enable button should not be visible");
+ let disableButton = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "enable-btn");
+ is_element_hidden(disableButton, "part3: disable button should not be visible");
+ let menu = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
+ is_element_visible(menu, "part3: state menu should be visible");
+ let askToActivateItem = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "ask-to-activate-menuitem");
+ is(menu.selectedItem, askToActivateItem, "part3: state menu should have 'Ask To Activate' selected");
+
+ let pluginTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gHttpTestRoot + "plugin_test.html");
+ let pluginBrowser = pluginTab.linkedBrowser;
+
+ let condition = () => PopupNotifications.getNotification("click-to-play-plugins", pluginBrowser);
+ yield BrowserTestUtils.waitForCondition(condition, "part4: should have a click-to-play notification");
+
+ yield BrowserTestUtils.removeTab(pluginTab);
+
+ let alwaysActivateItem = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "always-activate-menuitem");
+ menu.selectedItem = alwaysActivateItem;
+ alwaysActivateItem.doCommand();
+
+ pluginTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gHttpTestRoot + "plugin_test.html");
+
+ yield ContentTask.spawn(pluginTab.linkedBrowser, null, function*() {
+ let testPlugin = content.document.getElementById("test");
+ ok(testPlugin, "part5: should have a plugin element in the page");
+ let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let condition = () => objLoadingContent.activated;
+ yield ContentTaskUtils.waitForCondition(condition, "part5: waited too long for plugin to activate");
+ ok(objLoadingContent.activated, "part6: plugin should be activated");
+ });
+
+ yield BrowserTestUtils.removeTab(pluginTab);
+
+ let neverActivateItem = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "never-activate-menuitem");
+ menu.selectedItem = neverActivateItem;
+ neverActivateItem.doCommand();
+
+ pluginTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gHttpTestRoot + "plugin_test.html");
+ pluginBrowser = pluginTab.linkedBrowser;
+
+ yield ContentTask.spawn(pluginTab.linkedBrowser, null, function*() {
+ let testPlugin = content.document.getElementById("test");
+ ok(testPlugin, "part7: should have a plugin element in the page");
+ let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ ok(!objLoadingContent.activated, "part7: plugin should not be activated");
+ });
+
+ yield BrowserTestUtils.removeTab(pluginTab);
+
+ let details = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
+ is_element_visible(details, "part7: details link should be visible");
+ EventUtils.synthesizeMouseAtCenter(details, {}, managerWindow);
+ yield BrowserTestUtils.waitForEvent(managerWindow.document, "ViewChanged");
+
+ is_element_hidden(enableButton, "part8: detail enable button should be hidden");
+ is_element_hidden(disableButton, "part8: detail disable button should be hidden");
+ is_element_visible(menu, "part8: detail state menu should be visible");
+ is(menu.selectedItem, neverActivateItem, "part8: state menu should have 'Never Activate' selected");
+
+ menu.selectedItem = alwaysActivateItem;
+ alwaysActivateItem.doCommand();
+
+ pluginTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gHttpTestRoot + "plugin_test.html");
+ pluginBrowser = pluginTab.linkedBrowser;
+
+ yield ContentTask.spawn(pluginTab.linkedBrowser, null, function*() {
+ let testPlugin = content.document.getElementById("test");
+ ok(testPlugin, "part9: should have a plugin element in the page");
+ let objLoadingContent = testPlugin.QueryInterface(Ci.nsIObjectLoadingContent);
+ let condition = () => objLoadingContent.activated;
+ yield ContentTaskUtils.waitForCondition(condition, "part9: waited too long for plugin to activate");
+ ok(objLoadingContent.activated, "part10: plugin should be activated");
+ });
+
+ yield BrowserTestUtils.removeTab(pluginTab);
+
+ menu.selectedItem = askToActivateItem;
+ askToActivateItem.doCommand();
+
+ pluginTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, gHttpTestRoot + "plugin_test.html");
+ pluginBrowser = pluginTab.linkedBrowser;
+
+ condition = () => PopupNotifications.getNotification("click-to-play-plugins", pluginBrowser);
+ yield BrowserTestUtils.waitForCondition(condition, "part11: should have a click-to-play notification");
+
+ yield BrowserTestUtils.removeTab(pluginTab);
+
+ // causes appDisabled to be set
+ managerWindow = yield new Promise(resolve => {
+ setAndUpdateBlocklist(gHttpTestRoot + "blockPluginHard.xml",
+ () => {
+ close_manager(managerWindow, function() {
+ open_manager("addons://list/plugin", resolve);
+ });
+ }
+ );
+ });
+
+ pluginEl = get_addon_element(managerWindow, testPluginId);
+ pluginEl.parentNode.ensureElementIsVisible(pluginEl);
+ menu = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "state-menulist");
+ is(menu.disabled, true, "part12: state menu should be disabled");
+
+ details = managerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(details, {}, managerWindow);
+ yield BrowserTestUtils.waitForEvent(managerWindow.document, "ViewChanged");
+
+ menu = managerWindow.document.getElementById("detail-state-menulist");
+ is(menu.disabled, true, "part13: detail state menu should be disabled");
+
+ managerWindow.close();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_about.js b/toolkit/mozapps/extensions/test/browser/browser_about.js
new file mode 100644
index 000000000..f781cf146
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_about.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests the default and custom "about" dialogs of add-ons.
+ *
+ * Test for bug 610661 <https://bugzilla.mozilla.org/show_bug.cgi?id=610661>:
+ * Addon object not passed to custom about dialogs.
+ */
+
+var gManagerWindow;
+
+const URI_ABOUT_DEFAULT = "chrome://mozapps/content/extensions/about.xul";
+const URI_ABOUT_CUSTOM = CHROMEROOT + "addon_about.xul";
+
+function test() {
+ requestLongerTimeout(2);
+
+ waitForExplicitFinish();
+
+ var gProvider = new MockProvider();
+ gProvider.createAddons([{
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ description: "foo"
+ },
+ {
+ id: "test2@tests.mozilla.org",
+ name: "Test add-on 2",
+ description: "bar",
+ aboutURL: URI_ABOUT_CUSTOM
+ }]);
+
+ open_manager("addons://list/extension", function(aManager) {
+ gManagerWindow = aManager;
+
+ test_about_window("Test add-on 1", URI_ABOUT_DEFAULT, function() {
+ test_about_window("Test add-on 2", URI_ABOUT_CUSTOM, function() {
+ close_manager(gManagerWindow, finish);
+ });
+ });
+ });
+}
+
+function test_about_window(aAddonItemName, aExpectedAboutUri, aCallback) {
+ var addonList = gManagerWindow.document.getElementById("addon-list");
+ for (var addonItem of addonList.childNodes) {
+ if (addonItem.hasAttribute("name") &&
+ addonItem.getAttribute("name") === aAddonItemName)
+ break;
+ }
+
+ info("Waiting for about dialog");
+ Services.ww.registerNotification(function TEST_ww_observer(aSubject, aTopic,
+ aData) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(TEST_ww_observer);
+
+ info("About dialog closed, waiting for focus on browser window");
+ waitForFocus(() => executeSoon(aCallback));
+ } else if (aTopic == "domwindowopened") {
+ info("About dialog opened, waiting for focus");
+
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ waitForFocus(function() {
+ info("Saw about dialog");
+
+ is(win.location,
+ aExpectedAboutUri,
+ "The correct add-on about window should have opened");
+
+ is(win.arguments && win.arguments[0] && win.arguments[0].name,
+ aAddonItemName,
+ "window.arguments[0] should refer to the add-on object");
+
+ executeSoon(() => win.close());
+ }, win);
+ }
+ });
+
+ gManagerWindow.gViewController.doCommand("cmd_showItemAbout",
+ addonItem.mAddon);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_addonrepository_performance.js b/toolkit/mozapps/extensions/test/browser/browser_addonrepository_performance.js
new file mode 100644
index 000000000..879d7331e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_addonrepository_performance.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the metadata request includes startup time measurements
+
+var tmp = {};
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm", tmp);
+var AddonRepository = tmp.AddonRepository;
+
+var gTelemetry = Cc["@mozilla.org/base/telemetry;1"].getService(Ci.nsITelemetry);
+var gManagerWindow;
+var gProvider;
+
+function parseParams(aQuery) {
+ let params = {};
+
+ for (let param of aQuery.split("&")) {
+ let [key, value] = param.split("=");
+ params[key] = value;
+ }
+
+ return params;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ var gSeenRequest = false;
+
+ gProvider = new MockProvider();
+ gProvider.createAddons([{
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on"
+ }]);
+
+ function observe(aSubject, aTopic, aData) {
+ aSubject.QueryInterface(Ci.nsIChannel);
+ let url = aSubject.URI.QueryInterface(Ci.nsIURL);
+ if (url.filePath != "/extensions-dummy/metadata") {
+ return;
+ }
+ info(url.query);
+
+ // Check if we encountered telemetry errors and turn the tests for which
+ // we don't have valid data into known failures.
+ let snapshot = gTelemetry.getHistogramById("STARTUP_MEASUREMENT_ERRORS")
+ .snapshot();
+
+ let tProcessValid = (snapshot.counts[0] == 0);
+ let tMainValid = tProcessValid && (snapshot.counts[2] == 0);
+ let tFirstPaintValid = tProcessValid && (snapshot.counts[5] == 0);
+ let tSessionRestoredValid = tProcessValid && (snapshot.counts[6] == 0);
+
+ let params = parseParams(url.query);
+
+ is(params.appOS, Services.appinfo.OS, "OS should be correct");
+ is(params.appVersion, Services.appinfo.version, "Version should be correct");
+
+ if (tMainValid) {
+ ok(params.tMain >= 0, "Should be a sensible tMain");
+ } else {
+ todo(false, "An error occurred while recording the startup timestamps, skipping this test");
+ }
+
+ if (tFirstPaintValid) {
+ ok(params.tFirstPaint >= 0, "Should be a sensible tFirstPaint");
+ } else {
+ todo(false, "An error occurred while recording the startup timestamps, skipping this test");
+ }
+
+ if (tSessionRestoredValid) {
+ ok(params.tSessionRestored >= 0, "Should be a sensible tSessionRestored");
+ } else {
+ todo(false, "An error occurred while recording the startup timestamps, skipping this test");
+ }
+
+ gSeenRequest = true;
+ }
+
+ const PREF = "extensions.getAddons.getWithPerformance.url";
+
+ // Watch HTTP requests
+ Services.obs.addObserver(observe, "http-on-modify-request", false);
+ Services.prefs.setCharPref(PREF,
+ "http://127.0.0.1:8888/extensions-dummy/metadata?appOS=%OS%&appVersion=%VERSION%&tMain=%TIME_MAIN%&tFirstPaint=%TIME_FIRST_PAINT%&tSessionRestored=%TIME_SESSION_RESTORED%");
+
+ registerCleanupFunction(function() {
+ Services.obs.removeObserver(observe, "http-on-modify-request");
+ });
+
+ AddonRepository._beginGetAddons(["test1@tests.mozilla.org"], {
+ searchFailed: function() {
+ ok(gSeenRequest, "Should have seen metadata request");
+ finish();
+ }
+ }, true);
+}
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug523784.js b/toolkit/mozapps/extensions/test/browser/browser_bug523784.js
new file mode 100644
index 000000000..e61fafd6b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug523784.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const URI_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+// This tests that the blocklist dialog still affects soft-blocked add-ons
+// if the user clicks the "Restart Later" button. It also ensures that the
+// "Cancel" button is correctly renamed (to "Restart Later").
+var args = {
+ restart: false,
+ list: [{
+ name: "Bug 523784 softblocked addon",
+ version: "1",
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: false,
+ url: 'http://example.com/bug523784_1',
+ }],
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ let windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened")
+ return;
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function() {
+ win.removeEventListener("load", arguments.callee, false);
+
+ executeSoon(() => bug523784_test1(win));
+ }, false);
+ };
+ Services.ww.registerNotification(windowObserver);
+
+ args.wrappedJSObject = args;
+ Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+ "chrome,centerscreen,dialog,titlebar", args);
+}
+
+function bug523784_test1(win) {
+ let bundle = Services.strings.
+ createBundle("chrome://mozapps/locale/update/updates.properties");
+ let cancelButton = win.document.documentElement.getButton("cancel");
+ let moreInfoLink = win.document.getElementById("moreInfo");
+
+ is(cancelButton.getAttribute("label"),
+ bundle.GetStringFromName("restartLaterButton"),
+ "Text should be changed on Cancel button");
+ is(cancelButton.getAttribute("accesskey"),
+ bundle.GetStringFromName("restartLaterButton.accesskey"),
+ "Accesskey should also be changed on Cancel button");
+ is(moreInfoLink.getAttribute("href"),
+ 'http://example.com/bug523784_1',
+ "More Info link should link to a detailed blocklist page.");
+ let windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowclosed")
+ return;
+
+ Services.ww.unregisterNotification(windowObserver);
+
+ ok(args.list[0].disable, "Should be blocking add-on");
+ ok(!args.restart, "Should not restart browser immediately");
+
+ executeSoon(bug523784_test2);
+ };
+ Services.ww.registerNotification(windowObserver);
+
+ cancelButton.doCommand();
+}
+
+function bug523784_test2(win) {
+ let windowObserver = function(aSubject, aTopic, aData) {
+ if (aTopic != "domwindowopened")
+ return;
+
+ Services.ww.unregisterNotification(windowObserver);
+ let win = aSubject.QueryInterface(Ci.nsIDOMWindow);
+ win.addEventListener("load", function() {
+ win.removeEventListener("load", arguments.callee, false);
+
+ executeSoon(function() {
+ let moreInfoLink = win.document.getElementById("moreInfo");
+ let cancelButton = win.document.documentElement.getButton("cancel");
+ is(moreInfoLink.getAttribute("href"),
+ Services.urlFormatter.formatURLPref("extensions.blocklist.detailsURL"),
+ "More Info link should link to the general blocklist page.");
+ cancelButton.doCommand();
+ executeSoon(finish);
+ })
+ }, false);
+ };
+ Services.ww.registerNotification(windowObserver);
+
+ // Add 2 more addons to the blocked list to check that the more info link
+ // points to the general blocked list page.
+ args.list.push({
+ name: "Bug 523784 softblocked addon 2",
+ version: "2",
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: false,
+ url: 'http://example.com/bug523784_2'
+ });
+ args.list.push({
+ name: "Bug 523784 softblocked addon 3",
+ version: "4",
+ icon: "chrome://mozapps/skin/plugins/pluginGeneric.png",
+ disable: false,
+ blocked: false,
+ url: 'http://example.com/bug523784_3'
+ });
+
+ args.wrappedJSObject = args;
+ Services.ww.openWindow(null, URI_BLOCKLIST_DIALOG, "",
+ "chrome,centerscreen,dialog,titlebar", args);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557943.js b/toolkit/mozapps/extensions/test/browser/browser_bug557943.js
new file mode 100644
index 000000000..94a8b6f49
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557943.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 557943 - Searching for addons can result in wrong results
+
+var gManagerWindow;
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Microsoft .NET Framework Assistant",
+ description: "",
+ version: "6.66"
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "AwesomeNet Addon",
+ description: ""
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "Dictionnaire MySpell en Francais (réforme 1990)",
+ description: ""
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+
+function perform_search(aQuery, aCallback) {
+ waitForFocus(function() {
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = aQuery;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+ aCallback(rows);
+ });
+ }, gManagerWindow);
+}
+
+
+add_test(function() {
+ perform_search(".net", function(aRows) {
+ is(aRows.length, 1, "Should only get one result");
+ is(aRows[0].mAddon.id, "addon1@tests.mozilla.org", "Should get expected addon as only result");
+ run_next_test();
+ });
+});
+
+add_test(function() {
+ perform_search("réf", function(aRows) {
+ is(aRows.length, 1, "Should only get one result");
+ is(aRows[0].mAddon.id, "addon3@tests.mozilla.org", "Should get expected addon as only result");
+ run_next_test();
+ });
+});
+
+add_test(function() {
+ perform_search("javascript:void()", function(aRows) {
+ is(aRows.length, 0, "Should not get any results");
+ run_next_test();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557956.js b/toolkit/mozapps/extensions/test/browser/browser_bug557956.js
new file mode 100644
index 000000000..136e7cb74
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956.js
@@ -0,0 +1,524 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the compatibility dialog that displays during startup when the browser
+// version changes.
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_MIN_APP_COMPAT = "extensions.minCompatibleAppVersion";
+const PREF_MIN_PLATFORM_COMPAT = "extensions.minCompatiblePlatformVersion";
+
+Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+// avoid the 'leaked window property' check
+var scope = {};
+Components.utils.import("resource://gre/modules/TelemetrySession.jsm", scope);
+var TelemetrySession = scope.TelemetrySession;
+
+/**
+ * Test add-ons:
+ *
+ * Addon minVersion maxVersion Notes
+ * addon1 0 *
+ * addon2 0 0
+ * addon3 0 0
+ * addon4 1 *
+ * addon5 0 0 Made compatible by update check
+ * addon6 0 0 Made compatible by update check
+ * addon7 0 0 Has a broken update available
+ * addon8 0 0 Has an update available
+ * addon9 0 0 Has an update available
+ */
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ run_next_test();
+}
+
+function end_test() {
+ // Test generates a lot of available installs so just cancel them all
+ AddonManager.getAllInstalls(function(aInstalls) {
+ for (let install of aInstalls)
+ install.cancel();
+
+ Services.prefs.clearUserPref(PREF_MIN_APP_COMPAT);
+ Services.prefs.clearUserPref(PREF_MIN_PLATFORM_COMPAT);
+
+ finish();
+ });
+}
+
+function install_test_addons(aCallback) {
+ var installs = [];
+
+ // Use a blank update URL
+ Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+ let names = ["browser_bug557956_1",
+ "browser_bug557956_2",
+ "browser_bug557956_3",
+ "browser_bug557956_4",
+ "browser_bug557956_5",
+ "browser_bug557956_6",
+ "browser_bug557956_7",
+ "browser_bug557956_8_1",
+ "browser_bug557956_9_1",
+ "browser_bug557956_10"];
+ for (let name of names) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/" + name + ".xpi", function(aInstall) {
+ installs.push(aInstall);
+ }, "application/x-xpinstall");
+ }
+
+ var listener = {
+ installCount: 0,
+
+ onInstallEnded: function() {
+ this.installCount++;
+ if (this.installCount == installs.length) {
+ // Switch to the test update URL
+ Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "browser_bug557956.rdf");
+
+ executeSoon(aCallback);
+ }
+ }
+ };
+
+ for (let install of installs) {
+ install.addListener(listener);
+ install.install();
+ }
+}
+
+function uninstall_test_addons(aCallback) {
+ AddonManager.getAddonsByIDs(["bug557956-1@tests.mozilla.org",
+ "bug557956-2@tests.mozilla.org",
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-4@tests.mozilla.org",
+ "bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org",
+ "bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org",
+ "bug557956-10@tests.mozilla.org"],
+ function(aAddons) {
+ for (let addon of aAddons) {
+ if (addon)
+ addon.uninstall();
+ }
+ aCallback();
+ });
+}
+
+// Open the compatibility dialog, with the list of addon IDs
+// that were disabled by this "update"
+function open_compatibility_window(aDisabledAddons, aCallback) {
+ // This will reset the longer timeout multiplier to 2 which will give each
+ // test that calls open_compatibility_window a minimum of 60 seconds to
+ // complete.
+ requestLongerTimeout(2);
+
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(aDisabledAddons);
+
+ // Cannot be modal as we want to interact with it, shouldn't cause problems
+ // with testing though.
+ var features = "chrome,centerscreen,dialog,titlebar";
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+ win.addEventListener("load", function() {
+ win.removeEventListener("load", arguments.callee, false);
+
+ info("Compatibility dialog opened");
+
+ function page_shown(aEvent) {
+ if (aEvent.target.pageid)
+ info("Page " + aEvent.target.pageid + " shown");
+ }
+
+ win.addEventListener("pageshow", page_shown, false);
+ win.addEventListener("unload", function() {
+ win.removeEventListener("unload", arguments.callee, false);
+ win.removeEventListener("pageshow", page_shown, false);
+ info("Compatibility dialog closed");
+ }, false);
+
+ aCallback(win);
+ }, false);
+}
+
+function wait_for_window_close(aWindow, aCallback) {
+ aWindow.addEventListener("unload", function() {
+ aWindow.removeEventListener("unload", arguments.callee, false);
+ aCallback();
+ }, false);
+}
+
+function wait_for_page(aWindow, aPageId, aCallback) {
+ var page = aWindow.document.getElementById(aPageId);
+ page.addEventListener("pageshow", function() {
+ page.removeEventListener("pageshow", arguments.callee, false);
+ executeSoon(function() {
+ aCallback(aWindow);
+ });
+ }, false);
+}
+
+function get_list_names(aList) {
+ var items = [];
+ for (let listItem of aList.childNodes)
+ items.push(listItem.label);
+ items.sort();
+ return items;
+}
+
+function check_telemetry({disabled, metaenabled, metadisabled, upgraded, failed, declined}) {
+ let ping = TelemetrySession.getPayload();
+ // info(JSON.stringify(ping));
+ let am = ping.simpleMeasurements.addonManager;
+ if (disabled !== undefined)
+ is(am.appUpdate_disabled, disabled, disabled + " add-ons disabled by version change");
+ if (metaenabled !== undefined)
+ is(am.appUpdate_metadata_enabled, metaenabled, metaenabled + " add-ons enabled by metadata");
+ if (metadisabled !== undefined)
+ is(am.appUpdate_metadata_disabled, metadisabled, metadisabled + " add-ons disabled by metadata");
+ if (upgraded !== undefined)
+ is(am.appUpdate_upgraded, upgraded, upgraded + " add-ons upgraded");
+ if (failed !== undefined)
+ is(am.appUpdate_upgradeFailed, failed, failed + " upgrades failed");
+ if (declined !== undefined)
+ is(am.appUpdate_upgradeDeclined, declined, declined + " upgrades declined");
+}
+
+add_test(function test_setup() {
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ registerCleanupFunction(function () {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+ run_next_test();
+});
+
+// Tests that the right add-ons show up in the mismatch dialog and updates can
+// be installed
+add_test(function basic_mismatch() {
+ install_test_addons(function() {
+ // These add-ons become disabled
+ var disabledAddonIds = [
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org",
+ "bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org"
+ ];
+
+ AddonManager.getAddonsByIDs(["bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org"],
+ function([a5, a6]) {
+ // Check starting (pre-update) conditions
+ ok(!a5.isCompatible, "bug557956-5 should not be compatible");
+ ok(!a6.isCompatible, "bug557956-6 should not be compatible");
+
+ open_compatibility_window(disabledAddonIds, function(aWindow) {
+ var doc = aWindow.document;
+ wait_for_page(aWindow, "mismatch", function(aWindow) {
+ var items = get_list_names(doc.getElementById("mismatch.incompatible"));
+ // Check that compatibility updates from individual add-on update checks were applied.
+ is(items.length, 4, "Should have seen 4 still incompatible items");
+ is(items[0], "Addon3 1.0", "Should have seen addon3 still incompatible");
+ is(items[1], "Addon7 1.0", "Should have seen addon7 still incompatible");
+ is(items[2], "Addon8 1.0", "Should have seen addon8 still incompatible");
+ is(items[3], "Addon9 1.0", "Should have seen addon9 still incompatible");
+
+ // If it wasn't disabled by this run, we don't try to enable it
+ ok(!a5.isCompatible, "bug557956-5 should not be compatible");
+ ok(a6.isCompatible, "bug557956-6 should be compatible");
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "found", function(aWindow) {
+ ok(doc.getElementById("xpinstallDisabledAlert").hidden,
+ "Install should be allowed");
+
+ var list = doc.getElementById("found.updates");
+ var items = get_list_names(list);
+ is(items.length, 3, "Should have seen 3 updates available");
+ is(items[0], "Addon7 2.0", "Should have seen update for addon7");
+ is(items[1], "Addon8 2.0", "Should have seen update for addon8");
+ is(items[2], "Addon9 2.0", "Should have seen update for addon9");
+
+ ok(!doc.documentElement.getButton("next").disabled,
+ "Next button should be enabled");
+
+ // Uncheck all
+ for (let listItem of list.childNodes)
+ EventUtils.synthesizeMouse(listItem, 2, 2, { }, aWindow);
+
+ ok(doc.documentElement.getButton("next").disabled,
+ "Next button should not be enabled");
+
+ // Check the ones we want to install
+ for (let listItem of list.childNodes) {
+ if (listItem.label != "Addon7 2.0")
+ EventUtils.synthesizeMouse(listItem, 2, 2, { }, aWindow);
+ }
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "finished", function(aWindow) {
+ var button = doc.documentElement.getButton("finish");
+ ok(!button.hidden, "Finish button should not be hidden");
+ ok(!button.disabled, "Finish button should not be disabled");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_window_close(aWindow, function() {
+ AddonManager.getAddonsByIDs(["bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org"],
+ function([a8, a9]) {
+ is(a8.version, "2.0", "bug557956-8 should have updated");
+ is(a9.version, "2.0", "bug557956-9 should have updated");
+
+ check_telemetry({disabled: 5, metaenabled: 1, metadisabled: 0,
+ upgraded: 2, failed: 0, declined: 1});
+
+ uninstall_test_addons(run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that the install failures show the install failed page and disabling
+// xpinstall shows the right UI.
+add_test(function failure_page() {
+ install_test_addons(function() {
+ // These add-ons become disabled
+ var disabledAddonIds = [
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org",
+ "bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org"
+ ];
+
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+
+ open_compatibility_window(disabledAddonIds, function(aWindow) {
+ var doc = aWindow.document;
+ wait_for_page(aWindow, "mismatch", function(aWindow) {
+ var items = get_list_names(doc.getElementById("mismatch.incompatible"));
+ is(items.length, 4, "Should have seen 4 still incompatible items");
+ is(items[0], "Addon3 1.0", "Should have seen addon3 still incompatible");
+ is(items[1], "Addon7 1.0", "Should have seen addon7 still incompatible");
+ is(items[2], "Addon8 1.0", "Should have seen addon8 still incompatible");
+ is(items[3], "Addon9 1.0", "Should have seen addon9 still incompatible");
+
+ // Check that compatibility updates were applied.
+ AddonManager.getAddonsByIDs(["bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org"],
+ function([a5, a6]) {
+ ok(!a5.isCompatible, "bug557956-5 should not be compatible");
+ ok(a6.isCompatible, "bug557956-6 should be compatible");
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "found", function(aWindow) {
+ ok(!doc.getElementById("xpinstallDisabledAlert").hidden,
+ "Install should not be allowed");
+
+ ok(doc.documentElement.getButton("next").disabled,
+ "Next button should be disabled");
+
+ var checkbox = doc.getElementById("enableXPInstall");
+ EventUtils.synthesizeMouse(checkbox, 2, 2, { }, aWindow);
+
+ ok(!doc.documentElement.getButton("next").disabled,
+ "Next button should be enabled");
+
+ var list = doc.getElementById("found.updates");
+ var items = get_list_names(list);
+ is(items.length, 3, "Should have seen 3 updates available");
+ is(items[0], "Addon7 2.0", "Should have seen update for addon7");
+ is(items[1], "Addon8 2.0", "Should have seen update for addon8");
+ is(items[2], "Addon9 2.0", "Should have seen update for addon9");
+
+ // Unheck the ones we don't want to install
+ for (let listItem of list.childNodes) {
+ if (listItem.label != "Addon7 2.0")
+ EventUtils.synthesizeMouse(listItem, 2, 2, { }, aWindow);
+ }
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "installerrors", function(aWindow) {
+ var button = doc.documentElement.getButton("finish");
+ ok(!button.hidden, "Finish button should not be hidden");
+ ok(!button.disabled, "Finish button should not be disabled");
+
+ wait_for_window_close(aWindow, function() {
+ uninstall_test_addons(run_next_test);
+ });
+
+ check_telemetry({disabled: 5, metaenabled: 1, metadisabled: 0,
+ upgraded: 0, failed: 1, declined: 2});
+
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that no add-ons show up in the mismatch dialog when they are all disabled
+add_test(function all_disabled() {
+ install_test_addons(function() {
+ AddonManager.getAddonsByIDs(["bug557956-1@tests.mozilla.org",
+ "bug557956-2@tests.mozilla.org",
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-4@tests.mozilla.org",
+ "bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org",
+ "bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org",
+ "bug557956-10@tests.mozilla.org"],
+ function(aAddons) {
+ for (let addon of aAddons)
+ addon.userDisabled = true;
+
+ open_compatibility_window([], function(aWindow) {
+ // Should close immediately on its own
+ wait_for_window_close(aWindow, function() {
+ uninstall_test_addons(run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Tests that the right UI shows for when no updates are available
+add_test(function no_updates() {
+ install_test_addons(function() {
+ AddonManager.getAddonsByIDs(["bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org",
+ "bug557956-10@tests.mozilla.org"],
+ function(aAddons) {
+ for (let addon of aAddons)
+ addon.uninstall();
+
+ // These add-ons were disabled by the upgrade
+ var inactiveAddonIds = [
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org"
+ ];
+
+ open_compatibility_window(inactiveAddonIds, function(aWindow) {
+ var doc = aWindow.document;
+ wait_for_page(aWindow, "mismatch", function(aWindow) {
+ var items = get_list_names(doc.getElementById("mismatch.incompatible"));
+ is(items.length, 1, "Should have seen 1 still incompatible items");
+ is(items[0], "Addon3 1.0", "Should have seen addon3 still incompatible");
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "noupdates", function(aWindow) {
+ var button = doc.documentElement.getButton("finish");
+ ok(!button.hidden, "Finish button should not be hidden");
+ ok(!button.disabled, "Finish button should not be disabled");
+
+ wait_for_window_close(aWindow, function() {
+ uninstall_test_addons(run_next_test);
+ });
+
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that compatibility overrides are retrieved and affect addon
+// compatibility.
+add_test(function overrides_retrieved() {
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, false);
+ Services.prefs.setCharPref(PREF_MIN_APP_COMPAT, "0");
+ Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+ is(AddonManager.strictCompatibility, false, "Strict compatibility should be disabled");
+
+ // Use a blank update URL
+ Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+ install_test_addons(function() {
+
+ AddonManager.getAddonsByIDs(["bug557956-1@tests.mozilla.org",
+ "bug557956-2@tests.mozilla.org",
+ "bug557956-3@tests.mozilla.org",
+ "bug557956-4@tests.mozilla.org",
+ "bug557956-5@tests.mozilla.org",
+ "bug557956-6@tests.mozilla.org",
+ "bug557956-7@tests.mozilla.org",
+ "bug557956-8@tests.mozilla.org",
+ "bug557956-9@tests.mozilla.org",
+ "bug557956-10@tests.mozilla.org"],
+ function(aAddons) {
+
+ for (let addon of aAddons) {
+ if (addon.id == "bug557956-10@tests.mozilla.org")
+ is(addon.isCompatible, true, "Addon10 should be compatible before compat overrides are refreshed");
+ else
+ addon.uninstall();
+ }
+
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, TESTROOT + "browser_bug557956.xml");
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ open_compatibility_window([], function(aWindow) {
+ var doc = aWindow.document;
+ wait_for_page(aWindow, "mismatch", function(aWindow) {
+ var items = get_list_names(doc.getElementById("mismatch.incompatible"));
+ is(items.length, 1, "Should have seen 1 incompatible item");
+ is(items[0], "Addon10 1.0", "Should have seen addon10 as incompatible");
+
+ var button = doc.documentElement.getButton("next");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+
+ wait_for_page(aWindow, "noupdates", function(aWindow) {
+ var button = doc.documentElement.getButton("finish");
+ ok(!button.hidden, "Finish button should not be hidden");
+ ok(!button.disabled, "Finish button should not be disabled");
+
+ wait_for_window_close(aWindow, function() {
+ uninstall_test_addons(run_next_test);
+ });
+
+ check_telemetry({disabled: 0, metaenabled: 0, metadisabled: 1,
+ upgraded: 0, failed: 0, declined: 0});
+
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf b/toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf
new file mode 100644
index 000000000..c72eb9363
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf
@@ -0,0 +1,310 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-4@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-6@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-7@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_7_2.xpi</em:updateLink>
+ <em:updateHash>sha1:18674cf7ad76664e0ead6280a43cc0c681180505</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_7_2.xpi</em:updateLink>
+ <em:updateHash>sha1:18674cf7ad76664e0ead6280a43cc0c681180505</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-8@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpi</em:updateLink>
+ <em:updateHash>sha1:5691c398e55ddf93aa1076b9820619d21d40acbc</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpi</em:updateLink>
+ <em:updateHash>sha1:5691c398e55ddf93aa1076b9820619d21d40acbc</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug557956-9@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpi</em:updateLink>
+ <em:updateHash>sha1:1ae63bfc6f67a4503a1ff1bd02402c98fef19ae3</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpi</em:updateLink>
+ <em:updateHash>sha1:1ae63bfc6f67a4503a1ff1bd02402c98fef19ae3</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557956.xml b/toolkit/mozapps/extensions/test/browser/browser_bug557956.xml
new file mode 100644
index 000000000..c32ed3062
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1">
+ <addon_compatibility hosted="false">
+ <guid>bug557956-10@tests.mozilla.org</guid>
+ <name>Addon10</name>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>1.0</min_version>
+ <max_version>2.0</max_version>
+ <compatible_applications>
+ <application>
+ <min_version>0.1</min_version>
+ <max_version>999.0</max_version>
+ <appID>toolkit@mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpi b/toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpi
new file mode 100644
index 000000000..e99f3c3bd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956_8_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpi b/toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpi
new file mode 100644
index 000000000..23686b608
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug557956_9_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562797.js b/toolkit/mozapps/extensions/test/browser/browser_bug562797.js
new file mode 100644
index 000000000..55e882a05
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562797.js
@@ -0,0 +1,975 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests that history navigation works for the add-ons manager.
+ */
+
+const MAIN_URL = "https://example.com/" + RELATIVE_DIR + "discovery.html";
+const SECOND_URL = "https://example.com/" + RELATIVE_DIR + "releaseNotes.xhtml";
+
+var gLoadCompleteCallback = null;
+
+var gProgressListener = {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Only care about the network stop status events
+ if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_IS_NETWORK)) ||
+ !(aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP)))
+ return;
+
+ if (gLoadCompleteCallback)
+ executeSoon(gLoadCompleteCallback);
+ gLoadCompleteCallback = null;
+ },
+
+ onLocationChange: function() { },
+ onSecurityChange: function() { },
+ onProgressChange: function() { },
+ onStatusChange: function() { },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+};
+
+function waitForLoad(aManager, aCallback) {
+ var browser = aManager.document.getElementById("discover-browser");
+ browser.addProgressListener(gProgressListener);
+
+ gLoadCompleteCallback = function() {
+ browser.removeProgressListener(gProgressListener);
+ aCallback();
+ };
+}
+
+function clickLink(aManager, aId, aCallback) {
+ waitForLoad(aManager, aCallback);
+
+ var browser = aManager.document.getElementById("discover-browser");
+
+ var link = browser.contentDocument.getElementById(aId);
+ EventUtils.sendMouseEvent({type: "click"}, link);
+}
+
+function test() {
+ requestLongerTimeout(2);
+
+ waitForExplicitFinish();
+
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL);
+
+ SpecialPowers.pushPrefEnv({"set": [
+ ["dom.ipc.processCount", 1],
+ ]}, () => {
+ var gProvider = new MockProvider();
+ gProvider.createAddons([{
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ description: "foo"
+ },
+ {
+ id: "test2@tests.mozilla.org",
+ name: "Test add-on 2",
+ description: "bar"
+ },
+ {
+ id: "test3@tests.mozilla.org",
+ name: "Test add-on 3",
+ type: "theme",
+ description: "bar"
+ }]);
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+function go_back(aManager) {
+ if (gUseInContentUI) {
+ gBrowser.goBack();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("back-btn"),
+ { }, aManager);
+ }
+}
+
+function go_back_backspace(aManager) {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {});
+}
+
+function go_forward_backspace(aManager) {
+ EventUtils.synthesizeKey("VK_BACK_SPACE", {shiftKey: true});
+}
+
+function go_forward(aManager) {
+ if (gUseInContentUI) {
+ gBrowser.goForward();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("forward-btn"),
+ { }, aManager);
+ }
+}
+
+function check_state(aManager, canGoBack, canGoForward) {
+ var doc = aManager.document;
+
+ if (gUseInContentUI) {
+ is(gBrowser.canGoBack, canGoBack, "canGoBack should be correct");
+ is(gBrowser.canGoForward, canGoForward, "canGoForward should be correct");
+ }
+
+ if (!is_hidden(doc.getElementById("back-btn"))) {
+ is(!doc.getElementById("back-btn").disabled, canGoBack, "Back button should have the right state");
+ is(!doc.getElementById("forward-btn").disabled, canGoForward, "Forward button should have the right state");
+ }
+}
+
+function is_in_list(aManager, view, canGoBack, canGoForward) {
+ var doc = aManager.document;
+
+ is(doc.getElementById("categories").selectedItem.value, view, "Should be on the right category");
+ is(get_current_view(aManager).id, "list-view", "Should be on the right view");
+
+ check_state(aManager, canGoBack, canGoForward);
+}
+
+function is_in_search(aManager, query, canGoBack, canGoForward) {
+ var doc = aManager.document;
+
+ is(doc.getElementById("categories").selectedItem.value, "addons://search/", "Should be on the right category");
+ is(get_current_view(aManager).id, "search-view", "Should be on the right view");
+ is(doc.getElementById("header-search").value, query, "Should have used the right query");
+
+ check_state(aManager, canGoBack, canGoForward);
+}
+
+function is_in_detail(aManager, view, canGoBack, canGoForward) {
+ var doc = aManager.document;
+
+ is(doc.getElementById("categories").selectedItem.value, view, "Should be on the right category");
+ is(get_current_view(aManager).id, "detail-view", "Should be on the right view");
+
+ check_state(aManager, canGoBack, canGoForward);
+}
+
+function is_in_discovery(aManager, url, canGoBack, canGoForward) {
+ var browser = aManager.document.getElementById("discover-browser");
+
+ is(aManager.document.getElementById("discover-view").selectedPanel, browser,
+ "Browser should be visible");
+
+ var spec = browser.currentURI.spec;
+ var pos = spec.indexOf("#");
+ if (pos != -1)
+ spec = spec.substring(0, pos);
+
+ is(spec, url, "Should have loaded the right url");
+
+ check_state(aManager, canGoBack, canGoForward);
+}
+
+function double_click_addon_element(aManager, aId) {
+ var addon = get_addon_element(aManager, aId);
+ addon.parentNode.ensureElementIsVisible(addon);
+ EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 1 }, aManager);
+ EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 2 }, aManager);
+}
+
+// Tests simple forward and back navigation and that the right heading and
+// category is selected
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-plugin"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 5");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ double_click_addon_element(aManager, "test1@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 6");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 7");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that browsing to the add-ons manager from a website and going back works
+// Only relevant for in-content UI
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ function promiseViewLoad(manager) {
+ return new Promise(resolve => {
+ wait_for_view_load(manager, resolve);
+ });
+ }
+
+ function promiseManagerLoaded(manager) {
+ return new Promise(resolve => {
+ wait_for_manager_load(manager, resolve);
+ });
+ }
+
+ Task.spawn(function*() {
+ info("Part 1");
+ yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/", true, true);
+
+ info("Part 2");
+ ok(!gBrowser.canGoBack, "Should not be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:addons");
+ yield BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ let manager = yield promiseManagerLoaded(gBrowser.contentWindow.wrappedJSObject);
+
+ info("Part 3");
+ is_in_list(manager, "addons://list/extension", true, false);
+
+ // XXX: This is less than ideal, as it's currently difficult to deal with
+ // the browser frame switching between remote/non-remote in e10s mode.
+ let promiseLoaded;
+ if (gMultiProcessBrowser) {
+ promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ } else {
+ promiseLoaded = BrowserTestUtils.waitForEvent(gBrowser.selectedBrowser, "pageshow");
+ }
+
+ go_back(manager);
+ yield promiseLoaded;
+
+ info("Part 4");
+ is(gBrowser.currentURI.spec, "http://example.com/", "Should be showing the webpage");
+ ok(!gBrowser.canGoBack, "Should not be able to go back");
+ ok(gBrowser.canGoForward, "Should be able to go forward");
+
+ promiseLoaded = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ go_forward(manager);
+ yield promiseLoaded;
+
+ manager = yield promiseManagerLoaded(gBrowser.contentWindow.wrappedJSObject);
+ info("Part 5");
+ is_in_list(manager, "addons://list/extension", true, false);
+
+ close_manager(manager, run_next_test);
+ });
+});
+
+// Tests simple forward and back navigation and that the right heading and
+// category is selected -- Keyboard navigation [Bug 565359]
+// Only add the test if the backspace key navigates back and addon-manager
+// loaded in a tab
+add_test(function() {
+
+ if (!gUseInContentUI || (Services.prefs.getIntPref("browser.backspace_action") != 0)) {
+ run_next_test();
+ return;
+ }
+
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-plugin"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back_backspace(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward_backspace(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back_backspace(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 5");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ double_click_addon_element(aManager, "test1@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 6");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back_backspace(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 7");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+
+// Tests that opening a custom first view only stores a single history entry
+add_test(function() {
+ open_manager("addons://list/plugin", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-extension"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/extension", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+});
+
+
+// Tests that opening a view while the manager is already open adds a new
+// history entry
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ aManager.loadView("addons://list/plugin");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ go_forward(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Tests than navigating to a website and then going back returns to the
+// previous view
+// Only relevant for in-content UI
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ open_manager("addons://list/plugin", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ gBrowser.loadURI("http://example.com/");
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "http://example.com/")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+ info("Part 2");
+
+ executeSoon(function() {
+ ok(gBrowser.canGoBack, "Should be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ go_back(aManager);
+
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "about:addons")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ executeSoon(() => go_forward(aManager));
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "http://example.com/")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+ info("Part 4");
+
+ executeSoon(function() {
+ ok(gBrowser.canGoBack, "Should be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ go_back(aManager);
+
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "about:addons")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+ wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+ info("Part 5");
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ }, false);
+ });
+ }, false);
+ });
+ }, false);
+ });
+ }, false);
+ });
+});
+
+// Tests that going back to search results works
+add_test(function() {
+ // Before we open the add-ons manager, we should make sure that no filter
+ // has been set. If one is set, we remove it.
+ // This is for the check below, from bug 611459.
+ let store = Cc["@mozilla.org/xul/xulstore;1"].getService(Ci.nsIXULStore);
+ store.removeValue("about:addons", "search-filter-radiogroup", "value");
+
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ var search = aManager.document.getElementById("header-search");
+ search.focus();
+ search.value = "bar";
+ EventUtils.synthesizeKey("VK_RETURN", {}, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ // Remote search is meant to be checked by default (bug 611459), so we
+ // confirm that and then switch to a local search.
+ var localFilter = aManager.document.getElementById("search-filter-local");
+ var remoteFilter = aManager.document.getElementById("search-filter-remote");
+
+ is(remoteFilter.selected, true, "Remote filter should be set by default");
+
+ var list = aManager.document.getElementById("search-list");
+ list.ensureElementIsVisible(localFilter);
+ EventUtils.synthesizeMouseAtCenter(localFilter, { }, aManager);
+
+ is(localFilter.selected, true, "Should have changed to local filter");
+
+ // Now we continue with the normal test.
+
+ info("Part 2");
+ is_in_search(aManager, "bar", true, false);
+ check_all_in_list(aManager, ["test2@tests.mozilla.org", "test3@tests.mozilla.org"]);
+
+ double_click_addon_element(aManager, "test2@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_detail(aManager, "addons://search/", true, false);
+
+ go_back(aManager);
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_search(aManager, "bar", true, true);
+ check_all_in_list(aManager, ["test2@tests.mozilla.org", "test3@tests.mozilla.org"]);
+
+ go_forward(aManager);
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 5");
+ is_in_detail(aManager, "addons://search/", true, false);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that going back from a webpage to a detail view loaded from a search
+// result works
+// Only relevant for in-content UI
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ var search = aManager.document.getElementById("header-search");
+ search.focus();
+ search.value = "bar";
+ EventUtils.synthesizeKey("VK_RETURN", {});
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_search(aManager, "bar", true, false);
+ check_all_in_list(aManager, ["test2@tests.mozilla.org", "test3@tests.mozilla.org"]);
+
+ double_click_addon_element(aManager, "test2@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 3");
+ is_in_detail(aManager, "addons://search/", true, false);
+
+ gBrowser.loadURI("http://example.com/");
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "http://example.com/")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ info("Part 4");
+ executeSoon(function() {
+ ok(gBrowser.canGoBack, "Should be able to go back");
+ ok(!gBrowser.canGoForward, "Should not be able to go forward");
+
+ go_back(aManager);
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "about:addons")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+ info("Part 5");
+ is_in_detail(aManager, "addons://search/", true, true);
+
+ go_back(aManager);
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 6");
+ is_in_search(aManager, "bar", true, true);
+ check_all_in_list(aManager, ["test2@tests.mozilla.org", "test3@tests.mozilla.org"]);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ }, false);
+ });
+ }, false);
+ });
+ });
+ });
+});
+
+// Tests that refreshing a list view does not affect the history
+// Only relevant for in-content UI
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-plugin"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ gBrowser.reload();
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "about:addons")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+ info("Part 3");
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ }, false);
+ });
+ });
+});
+
+// Tests that refreshing a detail view does not affect the history
+// Only relevant for in-content UI
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ open_manager(null, function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ double_click_addon_element(aManager, "test1@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ gBrowser.reload();
+ gBrowser.addEventListener("pageshow", function(event) {
+ if (event.target.location != "about:addons")
+ return;
+ gBrowser.removeEventListener("pageshow", arguments.callee, false);
+
+ wait_for_view_load(gBrowser.contentWindow.wrappedJSObject, function(aManager) {
+ info("Part 3");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ go_back(aManager);
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 4");
+ is_in_list(aManager, "addons://list/extension", false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ }, false);
+ });
+ });
+});
+
+// Tests that removing an extension from the detail view goes back and doesn't
+// allow you to go forward again.
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension", false, false);
+
+ double_click_addon_element(aManager, "test1@tests.mozilla.org");
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_detail(aManager, "addons://list/extension", true, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("detail-uninstall-btn"),
+ { }, aManager);
+
+ wait_for_view_load(aManager, function() {
+ if (gUseInContentUI) {
+ // TODO until bug 590661 is fixed the back button will be enabled
+ // when displaying in content
+ is_in_list(aManager, "addons://list/extension", true, false);
+ } else {
+ is_in_list(aManager, "addons://list/extension", false, false);
+ }
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+});
+
+// Tests that the back and forward buttons only show up for windowed mode
+add_test(function() {
+ open_manager(null, function(aManager) {
+ var doc = aManager.document;
+
+ if (gUseInContentUI) {
+ var btn = document.getElementById("back-button");
+ if (!btn || is_hidden(btn)) {
+ is_element_visible(doc.getElementById("back-btn"), "Back button should not be hidden");
+ is_element_visible(doc.getElementById("forward-btn"), "Forward button should not be hidden");
+ } else {
+ is_element_hidden(doc.getElementById("back-btn"), "Back button should be hidden");
+ is_element_hidden(doc.getElementById("forward-btn"), "Forward button should be hidden");
+ }
+ } else {
+ is_element_visible(doc.getElementById("back-btn"), "Back button should not be hidden");
+ is_element_visible(doc.getElementById("forward-btn"), "Forward button should not be hidden");
+ }
+
+ close_manager(aManager, run_next_test);
+ });
+});
+
+// Tests that opening the manager opens the last view
+add_test(function() {
+ open_manager("addons://list/plugin", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ close_manager(aManager, function() {
+ open_manager(null, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+});
+
+// Tests that navigating the discovery page works when that was the first view
+add_test(function() {
+ open_manager("addons://discover/", function(aManager) {
+ info("1");
+ is_in_discovery(aManager, MAIN_URL, false, false);
+
+ clickLink(aManager, "link-good", function() {
+ info("2");
+ is_in_discovery(aManager, SECOND_URL, true, false);
+
+ waitForLoad(aManager, function() {
+ info("3");
+ is_in_discovery(aManager, MAIN_URL, false, true);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, SECOND_URL, true, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-plugin"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, SECOND_URL, true, true);
+
+ go_back(aManager);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, false, true);
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ });
+ });
+
+ go_forward(aManager);
+ });
+
+ go_back(aManager);
+ });
+ });
+});
+
+// Tests that navigating the discovery page works when that was the second view
+add_test(function() {
+ open_manager("addons://list/plugin", function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-discover"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, MAIN_URL, true, false);
+
+ clickLink(aManager, "link-good", function() {
+ is_in_discovery(aManager, SECOND_URL, true, false);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, true, true);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, SECOND_URL, true, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-plugin"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", true, false);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, SECOND_URL, true, true);
+
+ go_back(aManager);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, true, true);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ go_forward(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, MAIN_URL, true, true);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, SECOND_URL, true, true);
+
+ close_manager(aManager, run_next_test);
+ });
+
+ go_forward(aManager);
+ });
+ });
+ });
+ });
+ });
+ });
+
+ go_forward(aManager);
+ });
+
+ go_back(aManager);
+ });
+ });
+ });
+});
+
+// Tests that when displaying in-content and opened in the background the back
+// and forward buttons still appear when switching tabs
+add_test(function() {
+ if (!gUseInContentUI) {
+ run_next_test();
+ return;
+ }
+
+ var tab = gBrowser.addTab("about:addons");
+ var browser = gBrowser.getBrowserForTab(tab);
+
+ browser.addEventListener("pageshow", function(event) {
+ if (event.target.location.href != "about:addons")
+ return;
+ browser.removeEventListener("pageshow", arguments.callee, true);
+
+ wait_for_manager_load(browser.contentWindow.wrappedJSObject, function() {
+ wait_for_view_load(browser.contentWindow.wrappedJSObject, function(aManager) {
+ gBrowser.selectedTab = tab;
+
+ var doc = aManager.document;
+ var btn = document.getElementById("back-button");
+ if (!btn || is_hidden(btn)) {
+ is_element_visible(doc.getElementById("back-btn"), "Back button should not be hidden");
+ is_element_visible(doc.getElementById("forward-btn"), "Forward button should not be hidden");
+ } else {
+ is_element_hidden(doc.getElementById("back-btn"), "Back button should be hidden");
+ is_element_hidden(doc.getElementById("forward-btn"), "Forward button should be hidden");
+ }
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+ }, true);
+});
+
+// Tests that refreshing the disicovery pane integrates properly with history
+add_test(function() {
+ open_manager("addons://list/plugin", function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", false, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-discover"), { }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, MAIN_URL, true, false);
+
+ clickLink(aManager, "link-good", function() {
+ is_in_discovery(aManager, SECOND_URL, true, false);
+
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("category-discover"), { }, aManager);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, true, false);
+
+ go_back(aManager);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, SECOND_URL, true, true);
+
+ go_back(aManager);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, true, true);
+
+ go_back(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_list(aManager, "addons://list/plugin", false, true);
+
+ go_forward(aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ is_in_discovery(aManager, MAIN_URL, true, true);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, SECOND_URL, true, true);
+
+ waitForLoad(aManager, function() {
+ is_in_discovery(aManager, MAIN_URL, true, false);
+
+ close_manager(aManager, run_next_test);
+ });
+ go_forward(aManager);
+ });
+
+ go_forward(aManager);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562854.js b/toolkit/mozapps/extensions/test/browser/browser_bug562854.js
new file mode 100644
index 000000000..53e890b71
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562854.js
@@ -0,0 +1,129 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests that double-click does not go to detail view if the target is a link or button.
+ */
+
+function test() {
+ requestLongerTimeout(2);
+
+ waitForExplicitFinish();
+
+ var gProvider = new MockProvider();
+ gProvider.createAddons([{
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ description: "foo",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }]);
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+function is_in_list(aManager, view) {
+ var doc = aManager.document;
+
+ is(doc.getElementById("categories").selectedItem.value, view, "Should be on the right category");
+ is(get_current_view(aManager).id, "list-view", "Should be on the right view");
+}
+
+function is_in_detail(aManager, view) {
+ var doc = aManager.document;
+
+ is(doc.getElementById("categories").selectedItem.value, view, "Should be on the right category");
+ is(get_current_view(aManager).id, "detail-view", "Should be on the right view");
+}
+
+// Check that double-click does something.
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension");
+
+ var addon = get_addon_element(aManager, "test1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+ EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 1 }, aManager);
+ EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 2 }, aManager);
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_detail(aManager, "addons://list/extension");
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+});
+
+// Check that double-click does nothing when over the disable button.
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension");
+
+ var addon = get_addon_element(aManager, "test1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+ EventUtils.synthesizeMouseAtCenter(
+ aManager.document.getAnonymousElementByAttribute(addon, "anonid", "disable-btn"),
+ { clickCount: 1 },
+ aManager
+ );
+ // The disable button is replaced by the enable button when clicked on.
+ EventUtils.synthesizeMouseAtCenter(
+ aManager.document.getAnonymousElementByAttribute(addon, "anonid", "enable-btn"),
+ { clickCount: 2 },
+ aManager
+ );
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/extension");
+
+ close_manager(aManager, run_next_test);
+ });
+ });
+});
+
+// Check that double-click does nothing when over the undo button.
+add_test(function() {
+ open_manager("addons://list/extension", function(aManager) {
+ info("Part 1");
+ is_in_list(aManager, "addons://list/extension");
+
+ var addon = get_addon_element(aManager, "test1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+ EventUtils.synthesizeMouseAtCenter(
+ aManager.document.getAnonymousElementByAttribute(addon, "anonid", "remove-btn"),
+ { clickCount: 1 },
+ aManager
+ );
+
+ // The undo button is removed when clicked on.
+ // We need to wait for the UI to catch up.
+ setTimeout(function() {
+ var target = aManager.document.getAnonymousElementByAttribute(addon, "anonid", "undo-btn");
+ var rect = target.getBoundingClientRect();
+ var addonRect = addon.getBoundingClientRect();
+
+ EventUtils.synthesizeMouse(target, rect.width / 2, rect.height / 2, { clickCount: 1 }, aManager);
+ EventUtils.synthesizeMouse(addon,
+ rect.left - addonRect.left + rect.width / 2,
+ rect.top - addonRect.top + rect.height / 2,
+ { clickCount: 2 },
+ aManager
+ );
+
+ wait_for_view_load(aManager, function(aManager) {
+ info("Part 2");
+ is_in_list(aManager, "addons://list/extension");
+
+ close_manager(aManager, run_next_test);
+ });
+ }, 0);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562890.js b/toolkit/mozapps/extensions/test/browser/browser_bug562890.js
new file mode 100644
index 000000000..ccb12c489
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562890.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Tests the Preferences button for addons in list view
+ */
+
+function test() {
+ requestLongerTimeout(2);
+
+ waitForExplicitFinish();
+
+ var addonPrefsURI = CHROMEROOT + "addon_prefs.xul";
+
+ var gProvider = new MockProvider();
+ gProvider.createAddons([{
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ description: "foo"
+ },
+ {
+ id: "test2@tests.mozilla.org",
+ name: "Test add-on 2",
+ description: "bar",
+ optionsURL: addonPrefsURI
+ }]);
+
+ open_manager("addons://list/extension", function(aManager) {
+ var addonList = aManager.document.getElementById("addon-list");
+ for (var addonItem of addonList.childNodes) {
+ if (addonItem.hasAttribute("name") &&
+ addonItem.getAttribute("name") == "Test add-on 1")
+ break;
+ }
+ var prefsBtn = aManager.document.getAnonymousElementByAttribute(addonItem,
+ "anonid",
+ "preferences-btn");
+ is(prefsBtn.hidden, true, "Prefs button should be hidden for addon with no optionsURL set")
+
+ for (addonItem of addonList.childNodes) {
+ if (addonItem.hasAttribute("name") &&
+ addonItem.getAttribute("name") == "Test add-on 2")
+ break;
+ }
+ prefsBtn = aManager.document.getAnonymousElementByAttribute(addonItem,
+ "anonid",
+ "preferences-btn");
+ is(prefsBtn.hidden, false, "Prefs button should be shown for addon with a optionsURL set")
+
+ Services.ww.registerNotification(function TEST_ww_observer(aSubject, aTopic, aData) {
+ if (aTopic == "domwindowclosed") {
+ Services.ww.unregisterNotification(TEST_ww_observer);
+ // Give the preference window a chance to finish closing before closing
+ // the add-ons manager.
+ executeSoon(function() {
+ close_manager(aManager, finish);
+ });
+ } else if (aTopic == "domwindowopened") {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ win.addEventListener("load", function TEST_ww_onLoad() {
+ if (win.location != addonPrefsURI)
+ return;
+
+ win.removeEventListener("load", TEST_ww_onLoad, false);
+ is(win.location, addonPrefsURI,
+ "The correct addon pref window should have opened");
+ win.close();
+ }, false);
+ }
+ });
+
+ addonList.ensureElementIsVisible(addonItem);
+ EventUtils.synthesizeMouseAtCenter(prefsBtn, { }, aManager);
+ });
+
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562899.js b/toolkit/mozapps/extensions/test/browser/browser_bug562899.js
new file mode 100644
index 000000000..9807be98f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562899.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Simulates quickly switching between different list views to verify that only
+// the last selected is displayed
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", tempScope);
+var LightweightThemeManager = tempScope.LightweightThemeManager;
+
+const xpi = "browser/toolkit/mozapps/extensions/test/browser/browser_installssl.xpi";
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Add a lightweight theme so at least one theme exists
+ LightweightThemeManager.currentTheme = {
+ id: "test",
+ name: "Test lightweight theme",
+ headerURL: "http://example.com/header.png"
+ };
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ LightweightThemeManager.forgetUsedTheme("test");
+ finish();
+ });
+}
+
+// Tests that loading a second view before the first has not finished loading
+// does not merge the results
+add_test(function() {
+ var themeCount = null;
+ var pluginCount = null;
+ var themeItem = gCategoryUtilities.get("theme");
+ var pluginItem = gCategoryUtilities.get("plugin");
+ var list = gManagerWindow.document.getElementById("addon-list");
+
+ gCategoryUtilities.open(themeItem, function() {
+ themeCount = list.childNodes.length;
+ ok(themeCount > 0, "Test is useless if there are no themes");
+
+ gCategoryUtilities.open(pluginItem, function() {
+ pluginCount = list.childNodes.length;
+ ok(pluginCount > 0, "Test is useless if there are no plugins");
+
+ gCategoryUtilities.open(themeItem);
+
+ gCategoryUtilities.open(pluginItem, function() {
+ is(list.childNodes.length, pluginCount, "Should only see the plugins");
+
+ var item = list.firstChild;
+ while (item) {
+ is(item.getAttribute("type"), "plugin", "All items should be plugins");
+ item = item.nextSibling;
+ }
+
+ // Tests that switching to, from, to the same pane in quick succession
+ // still only shows the right number of results
+
+ gCategoryUtilities.open(themeItem);
+ gCategoryUtilities.open(pluginItem);
+ gCategoryUtilities.open(themeItem, function() {
+ is(list.childNodes.length, themeCount, "Should only see the theme");
+
+ var item = list.firstChild;
+ while (item) {
+ is(item.getAttribute("type"), "theme", "All items should be theme");
+ item = item.nextSibling;
+ }
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug562992.js b/toolkit/mozapps/extensions/test/browser/browser_bug562992.js
new file mode 100644
index 000000000..1cd4d90cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug562992.js
@@ -0,0 +1,70 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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 test ensures that when the extension manager UI is open and a
+ * restartless extension is installed from the web, its correct name appears
+ * when the download and installation complete. See bug 562992.
+ */
+
+var gManagerWindow;
+var gProvider;
+var gInstall;
+
+const EXTENSION_NAME = "Wunderbar";
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ open_manager("addons://list/extension", function (aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function () {
+ finish();
+ });
+}
+
+// Create a MockInstall with a MockAddon payload and add it to the provider,
+// causing the onNewInstall event to fire, which in turn will cause a new
+// "installing" item to appear in the list of extensions.
+add_test(function () {
+ let addon = new MockAddon(undefined, EXTENSION_NAME, "extension", true);
+ gInstall = new MockInstall(undefined, undefined, addon);
+ gInstall.addTestListener({
+ onNewInstall: run_next_test
+ });
+ gProvider.addInstall(gInstall);
+});
+
+// Finish the install, which will cause the "installing" item to be converted
+// to an "installed" item, which should have the correct add-on name.
+add_test(function () {
+ gInstall.addTestListener({
+ onInstallEnded: function () {
+ let list = gManagerWindow.document.getElementById("addon-list");
+
+ // To help prevent future breakage, don't assume the item is the only one
+ // in the list, or that it's first in the list. Find it by name.
+ for (let i = 0; i < list.itemCount; i++) {
+ let item = list.getItemAtIndex(i);
+ if (item.getAttribute("name") === EXTENSION_NAME) {
+ ok(true, "Item with correct name found");
+ run_next_test();
+ return;
+ }
+ }
+ ok(false, "Item with correct name was not found");
+ run_next_test();
+ }
+ });
+ gInstall.install();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug567127.js b/toolkit/mozapps/extensions/test/browser/browser_bug567127.js
new file mode 100644
index 000000000..1d9a75416
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug567127.js
@@ -0,0 +1,136 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests bug 567127 - Add install button to the add-ons manager
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+var gManagerWindow;
+var gSawInstallNotification = false;
+
+// This listens for the next opened window and checks it is of the right url.
+// opencallback is called when the new window is fully loaded
+// closecallback is called when the window is closed
+function WindowOpenListener(url, opencallback, closecallback) {
+ this.url = url;
+ this.opencallback = opencallback;
+ this.closecallback = closecallback;
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ wm.addListener(this);
+}
+
+WindowOpenListener.prototype = {
+ url: null,
+ opencallback: null,
+ closecallback: null,
+ window: null,
+ domwindow: null,
+
+ handleEvent: function(event) {
+ is(this.domwindow.document.location.href, this.url, "Should have opened the correct window");
+
+ this.domwindow.removeEventListener("load", this, false);
+ // Allow any other load handlers to execute
+ var self = this;
+ executeSoon(function() { self.opencallback(self.domwindow); } );
+ },
+
+ onWindowTitleChange: function(window, title) {
+ },
+
+ onOpenWindow: function(window) {
+ if (this.window)
+ return;
+
+ this.window = window;
+ this.domwindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ this.domwindow.addEventListener("load", this, false);
+ },
+
+ onCloseWindow: function(window) {
+ if (this.window != window)
+ return;
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ wm.removeListener(this);
+ this.opencallback = null;
+ this.window = null;
+ this.domwindow = null;
+
+ // Let the window close complete
+ executeSoon(this.closecallback);
+ this.closecallback = null;
+ }
+};
+
+
+var gInstallNotificationObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ var installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
+ if (gTestInWindow)
+ is(installInfo.browser, null, "Notification should have a null browser");
+ else
+ isnot(installInfo.browser, null, "Notification should have non-null browser");
+ gSawInstallNotification = true;
+ Services.obs.removeObserver(this, "addon-install-started");
+ }
+};
+
+
+function test_confirmation(aWindow, aExpectedURLs) {
+ var list = aWindow.document.getElementById("itemList");
+ is(list.childNodes.length, aExpectedURLs.length, "Should be the right number of installs");
+
+ for (let url of aExpectedURLs) {
+ let found = false;
+ for (let node of list.children) {
+ if (node.url == url) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "Should have seen " + url + " in the list");
+ }
+
+ aWindow.document.documentElement.cancelDialog();
+}
+
+add_task(function* test_install_from_file() {
+ gManagerWindow = yield open_manager("addons://list/extension");
+
+ var filePaths = [
+ get_addon_file_url("browser_bug567127_1.xpi"),
+ get_addon_file_url("browser_bug567127_2.xpi")
+ ];
+ MockFilePicker.returnFiles = filePaths.map(aPath => aPath.file);
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ // Set handler that executes the core test after the window opens,
+ // and resolves the promise when the window closes
+ let pInstallURIClosed = new Promise((resolve, reject) => {
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ try {
+ test_confirmation(aWindow, filePaths.map(aPath => aPath.spec));
+ } catch (e) {
+ reject(e);
+ }
+ }, resolve);
+ });
+
+ gManagerWindow.gViewController.doCommand("cmd_installFromFile");
+
+ yield pInstallURIClosed;
+
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+
+ MockFilePicker.cleanup();
+ yield close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug567137.js b/toolkit/mozapps/extensions/test/browser/browser_bug567137.js
new file mode 100644
index 000000000..bb9b9a894
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug567137.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that the selected category is persisted across loads of the manager
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager(null, function(aWindow) {
+ let utils = new CategoryUtilities(aWindow);
+
+ // Open the plugins category
+ utils.openType("plugin", function() {
+
+ // Re-open the manager
+ close_manager(aWindow, function() {
+ open_manager(null, function(aWindow) {
+ utils = new CategoryUtilities(aWindow);
+
+ is(utils.selectedCategory, "plugin", "Should have shown the plugins category");
+
+ // Open the extensions category
+ utils.openType("extension", function() {
+
+ // Re-open the manager
+ close_manager(aWindow, function() {
+ open_manager(null, function(aWindow) {
+ utils = new CategoryUtilities(aWindow);
+
+ is(utils.selectedCategory, "extension", "Should have shown the extensions category");
+ close_manager(aWindow, finish);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug570760.js b/toolkit/mozapps/extensions/test/browser/browser_bug570760.js
new file mode 100644
index 000000000..0606a9a31
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug570760.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("");
+
+// Bug 570760 - Make ctrl-f and / focus the search box in the add-ons manager
+
+var gManagerWindow;
+var focusCount = 0;
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ function focusHandler() {
+ searchBox.blur();
+ focusCount++;
+ }
+ searchBox.addEventListener("focus", focusHandler);
+ f_key_test();
+ slash_key_test();
+ searchBox.removeEventListener("focus", focusHandler);
+ end_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+function f_key_test() {
+ EventUtils.synthesizeKey("f", { accelKey: true }, gManagerWindow);
+ is(focusCount, 1, "Search box should have been focused due to the f key");
+}
+
+function slash_key_test() {
+ EventUtils.synthesizeKey("/", { }, gManagerWindow);
+ is(focusCount, 2, "Search box should have been focused due to the / key");
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug572561.js b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js
new file mode 100644
index 000000000..69102060e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug572561.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the locale category is shown if there are no locale packs
+// installed but some are pending install
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+var gInstallProperties = [{
+ name: "Locale Category Test",
+ type: "locale"
+}];
+var gInstall;
+var gExpectedCancel = false;
+var gTestInstallListener = {
+ onInstallStarted: function(aInstall) {
+ check_hidden(false);
+ },
+
+ onInstallEnded: function(aInstall) {
+ check_hidden(false);
+ run_next_test();
+ },
+
+ onInstallCancelled: function(aInstall) {
+ ok(gExpectedCancel, "Should expect install cancel");
+ check_hidden(false);
+ run_next_test();
+ },
+
+ onInstallFailed: function(aInstall) {
+ ok(false, "Did not expect onInstallFailed");
+ run_next_test();
+ }
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+function check_hidden(aExpectedHidden) {
+ var hidden = !gCategoryUtilities.isTypeVisible("locale");
+ is(hidden, aExpectedHidden, "Should have correct hidden state");
+}
+
+// Tests that a non-active install does not make the locale category show
+add_test(function() {
+ check_hidden(true);
+ gInstall = gProvider.createInstalls(gInstallProperties)[0];
+ gInstall.addTestListener(gTestInstallListener);
+ check_hidden(true);
+ run_next_test();
+});
+
+// Test that restarting the add-on manager with a non-active install
+// does not cause the locale category to show
+add_test(function() {
+ restart_manager(gManagerWindow, null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_hidden(true);
+ run_next_test();
+ });
+});
+
+// Test that installing the install shows the locale category
+add_test(function() {
+ gInstall.install();
+});
+
+// Test that restarting the add-on manager does not cause the locale category
+// to become hidden
+add_test(function() {
+ restart_manager(gManagerWindow, null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_hidden(false);
+
+ gExpectedCancel = true;
+ gInstall.cancel();
+ });
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug573062.js b/toolkit/mozapps/extensions/test/browser/browser_bug573062.js
new file mode 100644
index 000000000..6554451fb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug573062.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test() {
+ waitForExplicitFinish();
+
+ var gProvider = new MockProvider();
+ let perms = AddonManager.PERM_CAN_UNINSTALL |
+ AddonManager.PERM_CAN_ENABLE | AddonManager.PERM_CAN_DISABLE;
+
+ gProvider.createAddons([{
+ id: "restart-enable-disable@tests.mozilla.org",
+ name: "restart-enable-disable",
+ description: "foo",
+ permissions: perms,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_ENABLE |
+ AddonManager.OP_NEEDS_RESTART_DISABLE
+ },
+ {
+ id: "restart-uninstall@tests.mozilla.org",
+ name: "restart-uninstall",
+ description: "foo",
+ permissions: perms,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_UNINSTALL
+ },
+ {
+ id: "no-restart-required@tests.mozilla.org",
+ name: "no-restart-required",
+ description: "bar",
+ permissions: perms,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ let addonList = aWindow.document.getElementById("addon-list");
+ let ed_r_Item, un_r_Item, no_r_Item;
+ for (let addonItem of addonList.childNodes) {
+ let name = addonItem.getAttribute("name");
+ switch (name) {
+ case "restart-enable-disable":
+ ed_r_Item = addonItem;
+ break;
+ case "restart-uninstall":
+ un_r_Item = addonItem;
+ break;
+ case "no-restart-required":
+ no_r_Item = addonItem;
+ break;
+ }
+ }
+
+ // Check the buttons in the list view.
+ function checkTooltips(aItem, aEnable, aDisable, aRemove) {
+ is(aItem._enableBtn.getAttribute("tooltiptext"), aEnable);
+ is(aItem._disableBtn.getAttribute("tooltiptext"), aDisable);
+ is(aItem._removeBtn.getAttribute("tooltiptext"), aRemove);
+ }
+
+ let strs = aWindow.gStrings.ext;
+ addonList.selectedItem = ed_r_Item;
+ let ed_args = [ed_r_Item,
+ strs.GetStringFromName("enableAddonRestartRequiredTooltip"),
+ strs.GetStringFromName("disableAddonRestartRequiredTooltip"),
+ strs.GetStringFromName("uninstallAddonTooltip")];
+ checkTooltips.apply(null, ed_args);
+
+ addonList.selectedItem = un_r_Item;
+ let un_args = [un_r_Item,
+ strs.GetStringFromName("enableAddonTooltip"),
+ strs.GetStringFromName("disableAddonTooltip"),
+ strs.GetStringFromName("uninstallAddonRestartRequiredTooltip")];
+ checkTooltips.apply(null, un_args);
+
+ addonList.selectedItem = no_r_Item;
+ let no_args = [no_r_Item,
+ strs.GetStringFromName("enableAddonTooltip"),
+ strs.GetStringFromName("disableAddonTooltip"),
+ strs.GetStringFromName("uninstallAddonTooltip")];
+ checkTooltips.apply(null, no_args);
+
+ // Check the buttons in the details view.
+ function checkTooltips2(aItem, aEnable, aDisable, aRemove) {
+ let detailEnable = aWindow.document.getElementById("detail-enable-btn");
+ let detailDisable = aWindow.document.getElementById("detail-disable-btn");
+ let detailUninstall = aWindow.document.getElementById("detail-uninstall-btn");
+ ok(detailEnable.getAttribute("tooltiptext") == aEnable);
+ ok(detailDisable.getAttribute("tooltiptext") == aDisable);
+ ok(detailUninstall.getAttribute("tooltiptext") == aRemove);
+ }
+
+ function showInDetailView(aAddonId) {
+ aWindow.gViewController.loadView("addons://detail/" +
+ aWindow.encodeURIComponent(aAddonId));
+ }
+
+ // enable-disable:
+ showInDetailView("restart-enable-disable@tests.mozilla.org");
+ wait_for_view_load(aWindow, function() {
+ checkTooltips2.apply(null, ed_args);
+ // uninstall:
+ showInDetailView("restart-uninstall@tests.mozilla.org");
+ wait_for_view_load(aWindow, function() {
+ checkTooltips2.apply(null, un_args);
+ // no restart:
+ showInDetailView("no-restart-required@tests.mozilla.org");
+ wait_for_view_load(aWindow, function() {
+ checkTooltips2.apply(null, no_args);
+ aWindow.close();
+ finish();
+ });
+ });
+ });
+
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug577990.js b/toolkit/mozapps/extensions/test/browser/browser_bug577990.js
new file mode 100644
index 000000000..913b3b954
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug577990.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the visible delay in showing the "Language" category occurs
+// very minimally
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+var gInstall;
+var gInstallProperties = [{
+ name: "Locale Category Test",
+ type: "locale"
+}];
+
+function test() {
+ try {
+ if (Components.classes["@mozilla.org/gfx/info;1"].getService(Components.interfaces.nsIGfxInfo).D2DEnabled) {
+ requestLongerTimeout(2);
+ }
+ } catch (e) {}
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+function install_locale(aCallback) {
+ gInstall = gProvider.createInstalls(gInstallProperties)[0];
+ gInstall.addTestListener({
+ onInstallEnded: function(aInstall) {
+ gInstall.removeTestListener(this);
+ executeSoon(aCallback);
+ }
+ });
+ gInstall.install();
+}
+
+function check_hidden(aExpectedHidden) {
+ var hidden = !gCategoryUtilities.isTypeVisible("locale");
+ is(hidden, !!aExpectedHidden, "Should have correct hidden state");
+}
+
+function run_open_test(aTestSetup, aLoadHidden, aInitializedHidden, aSelected) {
+ function loadCallback(aManagerWindow) {
+ gManagerWindow = aManagerWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_hidden(aLoadHidden);
+ }
+
+ function run() {
+ open_manager(null, function() {
+ check_hidden(aInitializedHidden);
+ var selected = (gCategoryUtilities.selectedCategory == "locale");
+ is(selected, !!aSelected, "Should have correct selected state");
+
+ run_next_test();
+ }, loadCallback);
+ }
+
+ close_manager(gManagerWindow, function() {
+ // Allow for asynchronous functions to run before the manager opens
+ aTestSetup ? aTestSetup(run) : run();
+ });
+}
+
+
+// Tests that the locale category is hidden when there are no locales installed
+add_test(function() {
+ run_open_test(null, true, true);
+});
+
+// Tests that installing a locale while the Add-on Manager is open shows the
+// locale category
+add_test(function() {
+ check_hidden(true);
+ install_locale(function() {
+ check_hidden(false);
+ run_next_test();
+ });
+});
+
+// Tests that the locale category is shown with no delay when restarting
+// Add-on Manager
+add_test(function() {
+ run_open_test(null, false, false);
+});
+
+// Tests that cancelling the locale install and restarting the Add-on Manager
+// causes the locale category to be hidden with an acceptable delay
+add_test(function() {
+ gInstall.cancel();
+ run_open_test(null, false, true)
+});
+
+// Tests that the locale category is hidden with no delay when restarting
+// Add-on Manager
+add_test(function() {
+ run_open_test(null, true, true);
+});
+
+// Tests that installing a locale when the Add-on Manager is closed, and then
+// opening the Add-on Manager causes the locale category to be shown with an
+// acceptable delay
+add_test(function() {
+ run_open_test(install_locale, true, false);
+});
+
+// Tests that selection of the locale category persists
+add_test(function() {
+ gCategoryUtilities.openType("locale", function() {
+ run_open_test(null, false, false, true);
+ });
+});
+
+// Tests that cancelling the locale install and restarting the Add-on Manager
+// causes the locale category to be hidden and not selected
+add_test(function() {
+ gInstall.cancel();
+ run_open_test(null, false, true);
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug580298.js b/toolkit/mozapps/extensions/test/browser/browser_bug580298.js
new file mode 100644
index 000000000..d3d338203
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug580298.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that certain types of addons do not have their version number
+// displayed. This currently only includes lightweight themes.
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+add_task(function* test() {
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "extension@tests.mozilla.org",
+ name: "Extension 1",
+ type: "extension",
+ version: "123"
+ }, {
+ id: "theme@tests.mozilla.org",
+ name: "Theme 2",
+ type: "theme",
+ version: "456"
+ }, {
+ id: "lwtheme@personas.mozilla.org",
+ name: "Persona 3",
+ type: "theme",
+ version: "789"
+ }]);
+
+ gManagerWindow = yield open_manager();
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
+
+function get(aId) {
+ return gManagerWindow.document.getElementById(aId);
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function open_details(aList, aItem, aCallback) {
+ aList.ensureElementIsVisible(aItem);
+ EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(aItem, { clickCount: 2 }, gManagerWindow);
+ return new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+}
+
+var check_addon_has_version = Task.async(function*(aList, aName, aVersion) {
+ for (let i = 0; i < aList.itemCount; i++) {
+ let item = aList.getItemAtIndex(i);
+ if (get_node(item, "name").value === aName) {
+ ok(true, "Item with correct name found");
+ let { version } = yield get_tooltip_info(item);
+ is(version, aVersion, "Item has correct version");
+ return item;
+ }
+ }
+ ok(false, "Item with correct name was not found");
+ return null;
+});
+
+add_task(function*() {
+ yield gCategoryUtilities.openType("extension");
+ info("Extension");
+ let list = gManagerWindow.document.getElementById("addon-list");
+ let item = yield check_addon_has_version(list, "Extension 1", "123");
+ yield open_details(list, item);
+ is_element_visible(get("detail-version"), "Details view has version visible");
+ is(get("detail-version").value, "123", "Details view has correct version");
+});
+
+add_task(function*() {
+ yield gCategoryUtilities.openType("theme");
+ info("Normal theme");
+ let list = gManagerWindow.document.getElementById("addon-list");
+ let item = yield check_addon_has_version(list, "Theme 2", "456");
+ yield open_details(list, item);
+ is_element_visible(get("detail-version"), "Details view has version visible");
+ is(get("detail-version").value, "456", "Details view has correct version");
+});
+
+add_task(function*() {
+ yield gCategoryUtilities.openType("theme");
+ info("Lightweight theme");
+ let list = gManagerWindow.document.getElementById("addon-list");
+ // See that the version isn't displayed
+ let item = yield check_addon_has_version(list, "Persona 3", undefined);
+ yield open_details(list, item);
+ is_element_hidden(get("detail-version"), "Details view has version hidden");
+ // If the version element is hidden then we don't care about its value
+});
+
+add_task(function end_test() {
+ close_manager(gManagerWindow, finish);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug581076.js b/toolkit/mozapps/extensions/test/browser/browser_bug581076.js
new file mode 100644
index 000000000..b02a6cc3e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug581076.js
@@ -0,0 +1,132 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 581076 - No "See all results" link present when searching for add-ons and not all are displayed (extensions.getAddons.maxResults)
+
+const PREF_GETADDONS_BROWSESEARCHRESULTS = "extensions.getAddons.search.browseURL";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const PREF_GETADDONS_MAXRESULTS = "extensions.getAddons.maxResults";
+const SEARCH_URL = TESTROOT + "browser_searching.xml";
+const SEARCH_EXPECTED_TOTAL = 100;
+const SEARCH_QUERY = "search";
+
+const SEARCHABLE_PAGE = "addons://list/extension";
+
+var gManagerWindow;
+
+
+function test() {
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+
+ waitForExplicitFinish();
+
+ open_manager(SEARCHABLE_PAGE, function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ // Test generates a lot of available installs so just cancel them all
+ AddonManager.getAllInstalls(function(aInstalls) {
+ for (let install of aInstalls)
+ install.cancel();
+
+ close_manager(gManagerWindow, finish);
+ });
+}
+
+function search(aRemoteSearch, aCallback) {
+ waitForFocus(function() {
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = SEARCH_QUERY;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ let filter;
+ if (aRemoteSearch)
+ filter = gManagerWindow.document.getElementById("search-filter-remote");
+ else
+ filter = gManagerWindow.document.getElementById("search-filter-local");
+ EventUtils.synthesizeMouseAtCenter(filter, { }, gManagerWindow);
+
+ executeSoon(aCallback);
+ });
+ }, gManagerWindow);
+}
+
+function check_allresultslink(aShouldShow) {
+ var list = gManagerWindow.document.getElementById("search-list");
+ var link = gManagerWindow.document.getElementById("search-allresults-link");
+ is(link.parentNode, list.lastChild, "Footer should be at the end of the richlistbox");
+ if (aShouldShow) {
+ is_element_visible(link, "All Results link should be visible");
+ is(link.value, "See all " + SEARCH_EXPECTED_TOTAL + " results", "All Results link should show the correct message");
+ var scope = {};
+ Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm", scope);
+ is(link.href, scope.AddonRepository.getSearchURL(SEARCH_QUERY), "All Results link should have the correct href");
+ } else {
+ is_element_hidden(link, "All Results link should be hidden");
+ }
+}
+
+add_test(function() {
+ info("Searching locally");
+ search(false, function() {
+ check_allresultslink(false);
+ restart_manager(gManagerWindow, SEARCHABLE_PAGE, function(aManager) {
+ gManagerWindow = aManager;
+ run_next_test();
+ });
+ });
+});
+
+add_test(function() {
+ debugger;
+ info("Searching remotely - more results than cap");
+ Services.prefs.setIntPref(PREF_GETADDONS_MAXRESULTS, 3);
+ search(true, function() {
+ check_allresultslink(true);
+ restart_manager(gManagerWindow, SEARCHABLE_PAGE, function(aManager) {
+ gManagerWindow = aManager;
+ run_next_test();
+ });
+ });
+});
+
+add_test(function() {
+ info("Searching remotely - less results than cap");
+ Services.prefs.setIntPref(PREF_GETADDONS_MAXRESULTS, 200);
+ search(true, function() {
+ check_allresultslink(false);
+ restart_manager(gManagerWindow, SEARCHABLE_PAGE, function(aManager) {
+ gManagerWindow = aManager;
+ run_next_test();
+ });
+ });
+});
+
+add_test(function() {
+ info("Searching remotely - more results than cap");
+ Services.prefs.setIntPref(PREF_GETADDONS_MAXRESULTS, 3);
+ search(true, function() {
+ check_allresultslink(true);
+ run_next_test();
+ });
+});
+
+add_test(function() {
+ info("Switching views");
+ gManagerWindow.loadView("addons://list/extension");
+ wait_for_view_load(gManagerWindow, function() {
+ info("Re-loading previous search");
+ search(true, function() {
+ check_allresultslink(true);
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug586574.js b/toolkit/mozapps/extensions/test/browser/browser_bug586574.js
new file mode 100644
index 000000000..fb5ebf01b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug586574.js
@@ -0,0 +1,286 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 586574 - Provide way to set a default for automatic updates
+// Bug 710064 - Make the "Update Add-ons Automatically" checkbox state
+// also depend on the extensions.update.enabled pref
+
+// TEST_PATH=toolkit/mozapps/extensions/test/browser/browser_bug586574.js make -C obj-ff mochitest-browser-chrome
+
+const PREF_UPDATE_ENABLED = "extensions.update.enabled";
+const PREF_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+
+var gManagerWindow;
+var gProvider;
+
+var gUtilsBtn;
+var gUtilsMenu;
+var gDropdownMenu;
+var gSetDefault;
+var gResetToAutomatic;
+var gResetToManual;
+
+// Make sure we don't accidentally start a background update while the prefs
+// are enabled.
+disableBackgroundUpdateTimer();
+registerCleanupFunction(() => {
+ enableBackgroundUpdateTimer();
+});
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "addon 1",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+
+ gUtilsBtn = gManagerWindow.document.getElementById("header-utils-btn");
+ gUtilsMenu = gManagerWindow.document.getElementById("utils-menu");
+
+ run_next_test();
+ });
+}
+
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+
+function wait_for_popup(aCallback) {
+ if (gUtilsMenu.state == "open") {
+ aCallback();
+ return;
+ }
+
+ gUtilsMenu.addEventListener("popupshown", function() {
+ gUtilsMenu.removeEventListener("popupshown", arguments.callee, false);
+ info("Utilities menu shown");
+ aCallback();
+ }, false);
+}
+
+function wait_for_hide(aCallback) {
+ if (gUtilsMenu.state == "closed") {
+ aCallback();
+ return;
+ }
+
+ gUtilsMenu.addEventListener("popuphidden", function() {
+ gUtilsMenu.removeEventListener("popuphidden", arguments.callee, false);
+ info("Utilities menu hidden");
+ aCallback();
+ }, false);
+}
+
+add_test(function() {
+ gSetDefault = gManagerWindow.document.getElementById("utils-autoUpdateDefault");
+ gResetToAutomatic = gManagerWindow.document.getElementById("utils-resetAddonUpdatesToAutomatic");
+ gResetToManual = gManagerWindow.document.getElementById("utils-resetAddonUpdatesToManual");
+
+ info("Ensuring default prefs are set to true");
+ Services.prefs.setBoolPref(PREF_UPDATE_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_AUTOUPDATE_DEFAULT, true);
+
+ wait_for_popup(function() {
+ is(gSetDefault.getAttribute("checked"), "true",
+ "#1 Set Default menuitem should be checked");
+ is_element_visible(gResetToAutomatic,
+ "#1 Reset to Automatic menuitem should be visible");
+ is_element_hidden(gResetToManual,
+ "#1 Reset to Manual menuitem should be hidden");
+
+ var listener = {
+ onPropertyChanged: function(aAddon, aProperties) {
+ AddonManager.removeAddonListener(listener);
+ is(aAddon.id, gProvider.addons[0].id,
+ "Should get onPropertyChanged event for correct addon");
+ ok(!("applyBackgroundUpdates" in aProperties),
+ "Should have gotten applyBackgroundUpdates in properties array");
+ is(aAddon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT,
+ "Addon.applyBackgroundUpdates should have been reset to default");
+
+ info("Setting Addon.applyBackgroundUpdates back to disabled");
+ aAddon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ wait_for_hide(run_next_test);
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ info("Clicking Reset to Automatic menuitem");
+ EventUtils.synthesizeMouseAtCenter(gResetToAutomatic, { }, gManagerWindow);
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ info("Disabling extensions.update.enabled");
+ Services.prefs.setBoolPref(PREF_UPDATE_ENABLED, false);
+
+ wait_for_popup(function() {
+ isnot(gSetDefault.getAttribute("checked"), "true",
+ "#2 Set Default menuitem should not be checked");
+ is_element_visible(gResetToAutomatic,
+ "#2 Reset to Automatic menuitem should be visible");
+ is_element_hidden(gResetToManual,
+ "#2 Reset to Manual menuitem should be hidden");
+
+ wait_for_hide(run_next_test);
+
+ info("Clicking Set Default menuitem to reenable");
+ EventUtils.synthesizeMouseAtCenter(gSetDefault, { }, gManagerWindow);
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ ok(Services.prefs.getBoolPref(PREF_UPDATE_ENABLED),
+ "extensions.update.enabled should be true after the previous test");
+ ok(Services.prefs.getBoolPref(PREF_AUTOUPDATE_DEFAULT),
+ "extensions.update.autoUpdateDefault should be true after the previous test");
+
+ info("Disabling both extensions.update.enabled and extensions.update.autoUpdateDefault");
+ Services.prefs.setBoolPref(PREF_UPDATE_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_AUTOUPDATE_DEFAULT, false);
+
+ wait_for_popup(function() {
+ isnot(gSetDefault.getAttribute("checked"), "true",
+ "#3 Set Default menuitem should not be checked");
+ is_element_hidden(gResetToAutomatic,
+ "#3 Reset to automatic menuitem should be hidden");
+ is_element_visible(gResetToManual,
+ "#3 Reset to manual menuitem should be visible");
+
+ wait_for_hide(run_next_test);
+
+ info("Clicking Set Default menuitem to reenable");
+ EventUtils.synthesizeMouseAtCenter(gSetDefault, { }, gManagerWindow);
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ ok(Services.prefs.getBoolPref(PREF_UPDATE_ENABLED),
+ "extensions.update.enabled should be true after the previous test");
+ ok(Services.prefs.getBoolPref(PREF_AUTOUPDATE_DEFAULT),
+ "extensions.update.autoUpdateDefault should be true after the previous test");
+
+ info("clicking the button to disable extensions.update.autoUpdateDefault");
+ wait_for_popup(function() {
+ is(gSetDefault.getAttribute("checked"), "true",
+ "#4 Set Default menuitem should be checked");
+ is_element_visible(gResetToAutomatic,
+ "#4 Reset to Automatic menuitem should be visible");
+ is_element_hidden(gResetToManual,
+ "#4 Reset to Manual menuitem should be hidden");
+
+ wait_for_hide(run_next_test);
+
+ info("Clicking Set Default menuitem to disable");
+ EventUtils.synthesizeMouseAtCenter(gSetDefault, { }, gManagerWindow);
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ ok(Services.prefs.getBoolPref(PREF_UPDATE_ENABLED),
+ "extensions.update.enabled should be true after the previous test");
+ is(Services.prefs.getBoolPref(PREF_AUTOUPDATE_DEFAULT), false,
+ "extensions.update.autoUpdateDefault should be false after the previous test");
+
+ wait_for_popup(function() {
+ isnot(gSetDefault.getAttribute("checked"), "true",
+ "#5 Set Default menuitem should not be checked");
+ is_element_hidden(gResetToAutomatic,
+ "#5 Reset to automatic menuitem should be hidden");
+ is_element_visible(gResetToManual,
+ "#5 Reset to manual menuitem should be visible");
+
+ var listener = {
+ onPropertyChanged: function(aAddon, aProperties) {
+ AddonManager.removeAddonListener(listener);
+ is(aAddon.id, gProvider.addons[0].id,
+ "Should get onPropertyChanged event for correct addon");
+ ok(!("applyBackgroundUpdates" in aProperties),
+ "Should have gotten applyBackgroundUpdates in properties array");
+ is(aAddon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT,
+ "Addon.applyBackgroundUpdates should have been reset to default");
+
+ info("Setting Addon.applyBackgroundUpdates back to disabled");
+ aAddon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ wait_for_hide(run_next_test);
+ }
+ };
+ AddonManager.addAddonListener(listener);
+
+ info("Clicking Reset to Manual menuitem");
+ EventUtils.synthesizeMouseAtCenter(gResetToManual, { }, gManagerWindow);
+
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ wait_for_popup(function() {
+ isnot(gSetDefault.getAttribute("checked"), "true",
+ "#6 Set Default menuitem should not be checked");
+ is_element_hidden(gResetToAutomatic,
+ "#6 Reset to automatic menuitem should be hidden");
+ is_element_visible(gResetToManual,
+ "#6 Reset to manual menuitem should be visible");
+
+ wait_for_hide(run_next_test);
+
+ info("Clicking Set Default menuitem");
+ EventUtils.synthesizeMouseAtCenter(gSetDefault, { }, gManagerWindow);
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ wait_for_popup(function() {
+ is(gSetDefault.getAttribute("checked"), "true",
+ "#7 Set Default menuitem should be checked");
+ is_element_visible(gResetToAutomatic,
+ "#7 Reset to Automatic menuitem should be visible");
+ is_element_hidden(gResetToManual,
+ "#7 Reset to Manual menuitem should be hidden");
+
+ wait_for_hide(run_next_test);
+
+ gUtilsMenu.hidePopup();
+ });
+
+ info("Opening utilities menu");
+ EventUtils.synthesizeMouseAtCenter(gUtilsBtn, { }, gManagerWindow);
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug587970.js b/toolkit/mozapps/extensions/test/browser/browser_bug587970.js
new file mode 100644
index 000000000..ef05ba4ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug587970.js
@@ -0,0 +1,180 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 587970 - Provide ability "Update all now" within 'Available Updates' screen
+
+var gManagerWindow;
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "addon 1",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "addon 2",
+ version: "2.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "addon 3",
+ version: "3.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }]);
+
+
+ open_manager("addons://updates/available", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("updates-list");
+ is(list.childNodes.length, 0, "Available updates list should be empty");
+
+ var emptyNotice = gManagerWindow.document.getElementById("empty-availableUpdates-msg");
+ is_element_visible(emptyNotice, "Empty notice should be visible");
+
+ var updateSelected = gManagerWindow.document.getElementById("update-selected-btn");
+ is_element_hidden(updateSelected, "Update Selected button should be hidden");
+
+ info("Adding updates");
+ gProvider.createInstalls([{
+ name: "addon 1",
+ version: "1.1",
+ existingAddon: gProvider.addons[0]
+ }, {
+ name: "addon 2",
+ version: "2.1",
+ existingAddon: gProvider.addons[1]
+ }, {
+ name: "addon 3",
+ version: "3.1",
+ existingAddon: gProvider.addons[2]
+ }]);
+
+ function wait_for_refresh() {
+ if (list.childNodes.length == 3 &&
+ list.childNodes[0].mManualUpdate &&
+ list.childNodes[1].mManualUpdate &&
+ list.childNodes[2].mManualUpdate) {
+ run_next_test();
+ } else {
+ info("Waiting for pane to refresh");
+ setTimeout(wait_for_refresh, 10);
+ }
+ }
+ info("Waiting for pane to refresh");
+ setTimeout(wait_for_refresh, 10);
+});
+
+
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("updates-list");
+ is(list.childNodes.length, 3, "Available updates list should have 2 items");
+
+ var item1 = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ isnot(item1, null, "Item for addon1@tests.mozilla.org should be in list");
+ var item2 = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+ isnot(item2, null, "Item for addon2@tests.mozilla.org should be in list");
+ var item3 = get_addon_element(gManagerWindow, "addon3@tests.mozilla.org");
+ isnot(item3, null, "Item for addon3@tests.mozilla.org should be in list");
+
+ var emptyNotice = gManagerWindow.document.getElementById("empty-availableUpdates-msg");
+ is_element_hidden(emptyNotice, "Empty notice should be hidden");
+
+ var updateSelected = gManagerWindow.document.getElementById("update-selected-btn");
+ is_element_visible(updateSelected, "Update Selected button should be visible");
+ is(updateSelected.disabled, false, "Update Selected button should be enabled by default");
+
+ is(item1._includeUpdate.checked, true, "Include Update checkbox should be checked by default for addon1");
+ is(item2._includeUpdate.checked, true, "Include Update checkbox should be checked by default for addon2");
+ is(item3._includeUpdate.checked, true, "Include Update checkbox should be checked by default for addon3");
+
+ info("Unchecking Include Update checkbox for addon1");
+ EventUtils.synthesizeMouse(item1._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item1._includeUpdate.checked, false, "Include Update checkbox should now be be unchecked for addon1");
+ is(updateSelected.disabled, false, "Update Selected button should still be enabled");
+
+ info("Unchecking Include Update checkbox for addon2");
+ EventUtils.synthesizeMouse(item2._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item2._includeUpdate.checked, false, "Include Update checkbox should now be be unchecked for addon2");
+ is(updateSelected.disabled, false, "Update Selected button should still be enabled");
+
+ info("Unchecking Include Update checkbox for addon3");
+ EventUtils.synthesizeMouse(item3._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item3._includeUpdate.checked, false, "Include Update checkbox should now be be unchecked for addon3");
+ is(updateSelected.disabled, true, "Update Selected button should now be disabled");
+
+ info("Checking Include Update checkbox for addon2");
+ EventUtils.synthesizeMouse(item2._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item2._includeUpdate.checked, true, "Include Update checkbox should now be be checked for addon2");
+ is(updateSelected.disabled, false, "Update Selected button should now be enabled");
+
+ info("Checking Include Update checkbox for addon3");
+ EventUtils.synthesizeMouse(item3._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item3._includeUpdate.checked, true, "Include Update checkbox should now be be checked for addon3");
+ is(updateSelected.disabled, false, "Update Selected button should now be enabled");
+
+ var installCount = 0;
+ var listener = {
+ onDownloadStarted: function(aInstall) {
+ isnot(aInstall.existingAddon.id, "addon1@tests.mozilla.org", "Should not have seen a download start for addon1");
+ },
+
+ onInstallEnded: function(aInstall) {
+ if (++installCount < 2)
+ return;
+
+ gProvider.installs[0].removeTestListener(listener);
+ gProvider.installs[1].removeTestListener(listener);
+ gProvider.installs[2].removeTestListener(listener);
+
+ // Installs are started synchronously so by the time an executeSoon is
+ // executed all installs that are going to start will have started
+ executeSoon(function() {
+ is(gProvider.installs[0].state, AddonManager.STATE_AVAILABLE, "addon1 should not have been upgraded");
+ is(gProvider.installs[1].state, AddonManager.STATE_INSTALLED, "addon2 should have been upgraded");
+ is(gProvider.installs[2].state, AddonManager.STATE_INSTALLED, "addon3 should have been upgraded");
+
+ run_next_test();
+ });
+ }
+ }
+ gProvider.installs[0].addTestListener(listener);
+ gProvider.installs[1].addTestListener(listener);
+ gProvider.installs[2].addTestListener(listener);
+ info("Clicking Update Selected button");
+ EventUtils.synthesizeMouseAtCenter(updateSelected, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ var updateSelected = gManagerWindow.document.getElementById("update-selected-btn");
+ is(updateSelected.disabled, true, "Update Selected button should be disabled");
+
+ var item1 = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ isnot(item1, null, "Item for addon1@tests.mozilla.org should be in list");
+ is(item1._includeUpdate.checked, false, "Include Update checkbox should not have changed");
+
+ info("Checking Include Update checkbox for addon1");
+ EventUtils.synthesizeMouse(item1._includeUpdate, 2, 2, { }, gManagerWindow);
+ is(item1._includeUpdate.checked, true, "Include Update checkbox should now be be checked for addon1");
+ is(updateSelected.disabled, false, "Update Selected button should now not be disabled");
+
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug590347.js b/toolkit/mozapps/extensions/test/browser/browser_bug590347.js
new file mode 100644
index 000000000..f805f0e19
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug590347.js
@@ -0,0 +1,121 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 590347
+// Tests if softblock notifications are exposed in preference to incompatible
+// notifications when compatibility checking is disabled
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gVersion = Services.appinfo.version;
+
+// Opens the details view of an add-on
+function open_details(aId, aType, aCallback) {
+ requestLongerTimeout(2);
+
+ gCategoryUtilities.openType(aType, function() {
+ var list = gManagerWindow.document.getElementById("addon-list");
+ var item = list.firstChild;
+ while (item) {
+ if ("mAddon" in item && item.mAddon.id == aId) {
+ list.ensureElementIsVisible(item);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, aCallback);
+ return;
+ }
+ item = item.nextSibling;
+ }
+ ok(false, "Should have found the add-on in the list");
+ });
+}
+
+function get_list_view_warning_node() {
+ let item = gManagerWindow.document.getElementById("addon-list").firstChild;
+ let found = false;
+ while (item) {
+ if (item.mAddon.name == "Test add-on") {
+ found = true;
+ break;
+ }
+ item = item.nextSibling;
+ }
+ ok(found, "Test add-on node should have been found.");
+ return item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning");
+}
+
+function get_detail_view_warning_node(aManagerWindow) {
+ if (aManagerWindow)
+ return aManagerWindow.document.getElementById("detail-warning");
+ return undefined;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on",
+ description: "A test add-on",
+ isCompatible: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
+ }]);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+// Check with compatibility checking enabled
+add_test(function() {
+ gCategoryUtilities.openType("extension", function() {
+ Services.prefs.setBoolPref(PREF_CHECK_COMPATIBILITY, true);
+ let warning_node = get_list_view_warning_node();
+ is_element_visible(warning_node, "Warning message should be visible");
+ is(warning_node.textContent, "Test add-on is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ run_next_test();
+ });
+});
+
+add_test(function() {
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ let warning_node = get_detail_view_warning_node(gManagerWindow);
+ is_element_visible(warning_node, "Warning message should be visible");
+ is(warning_node.textContent, "Test add-on is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ Services.prefs.setBoolPref(PREF_CHECK_COMPATIBILITY, false);
+ run_next_test();
+ });
+});
+
+// Check with compatibility checking disabled
+add_test(function() {
+ gCategoryUtilities.openType("extension", function() {
+ let warning_node = get_list_view_warning_node();
+ is_element_visible(warning_node, "Warning message should be visible");
+ is(warning_node.textContent, "Test add-on is known to cause security or stability issues.", "Warning message should be correct");
+ run_next_test();
+ });
+});
+
+add_test(function() {
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ let warning_node = get_detail_view_warning_node(gManagerWindow);
+ is_element_visible(warning_node, "Warning message should be visible");
+ is(warning_node.textContent, "Test add-on is known to cause security or stability issues.", "Warning message should be correct");
+ run_next_test();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug591465.js b/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
new file mode 100644
index 000000000..f759ffc1f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug591465.js
@@ -0,0 +1,512 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 591465 - Context menu of add-ons miss context related state change entries
+
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", tempScope);
+var LightweightThemeManager = tempScope.LightweightThemeManager;
+
+
+const PREF_GETADDONS_MAXRESULTS = "extensions.getAddons.maxResults";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_bug591465.xml";
+const SEARCH_QUERY = "SEARCH";
+
+var gManagerWindow;
+var gProvider;
+var gContextMenu;
+var gLWTheme = {
+ id: "4",
+ version: "1",
+ name: "Bling",
+ description: "SO MUCH BLING!",
+ author: "Pixel Pusher",
+ homepageURL: "http://mochi.test:8888/data/index.html",
+ headerURL: "http://mochi.test:8888/data/header.png",
+ footerURL: "http://mochi.test:8888/data/footer.png",
+ previewURL: "http://mochi.test:8888/data/preview.png",
+ iconURL: "http://mochi.test:8888/data/icon.png"
+ };
+
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "addon 1",
+ version: "1.0"
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "addon 2",
+ version: "1.0",
+ _userDisabled: true
+ }, {
+ id: "theme1@tests.mozilla.org",
+ name: "theme 1",
+ version: "1.0",
+ type: "theme"
+ }, {
+ id: "theme2@tests.mozilla.org",
+ name: "theme 2",
+ version: "1.0",
+ type: "theme",
+ _userDisabled: true
+ }, {
+ id: "theme3@tests.mozilla.org",
+ name: "theme 3",
+ version: "1.0",
+ type: "theme",
+ permissions: 0
+ }]);
+
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gContextMenu = aWindow.document.getElementById("addonitem-popup");
+ run_next_test();
+ });
+}
+
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+
+function check_contextmenu(aIsTheme, aIsEnabled, aIsRemote, aIsDetails, aIsSingleItemCase) {
+ if (aIsTheme || aIsEnabled || aIsRemote)
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_enableItem"),
+ "'Enable' should be hidden");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_enableItem"),
+ "'Enable' should be visible");
+
+ if (aIsTheme || !aIsEnabled || aIsRemote)
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_disableItem"),
+ "'Disable' should be hidden");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_disableItem"),
+ "'Disable' should be visible");
+
+ if (!aIsTheme || aIsEnabled || aIsRemote || aIsSingleItemCase)
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_enableTheme"),
+ "'Wear Theme' should be hidden");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_enableTheme"),
+ "'Wear Theme' should be visible");
+
+ if (!aIsTheme || !aIsEnabled || aIsRemote || aIsSingleItemCase)
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_disableTheme"),
+ "'Stop Wearing Theme' should be hidden");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_disableTheme"),
+ "'Stop Wearing Theme' should be visible");
+
+ if (aIsRemote)
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_installItem"),
+ "'Install' should be visible");
+ else
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_installItem"),
+ "'Install' should be hidden");
+
+ if (aIsDetails)
+ is_element_hidden(gManagerWindow.document.getElementById("menuitem_showDetails"),
+ "'Show More Information' should be hidden in details view");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("menuitem_showDetails"),
+ "'Show More Information' should be visible in list view");
+
+ if (aIsSingleItemCase)
+ is_element_hidden(gManagerWindow.document.getElementById("addonitem-menuseparator"),
+ "Menu separator should be hidden with only one menu item");
+ else
+ is_element_visible(gManagerWindow.document.getElementById("addonitem-menuseparator"),
+ "Menu separator should be visible with multiple menu items");
+
+}
+
+
+add_test(function() {
+ var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ isnot(el, null, "Should have found addon element");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, true, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled extension item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+add_test(function() {
+ var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ isnot(el, null, "Should have found addon element");
+ el.mAddon.userDisabled = true;
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, false, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on newly disabled extension item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+add_test(function() {
+ var el = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ isnot(el, null, "Should have found addon element");
+ el.mAddon.userDisabled = false;
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, true, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on newly enabled extension item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+add_test(function() {
+ var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, false, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on disabled extension item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://list/theme");
+ wait_for_view_load(gManagerWindow, function() {
+ var el = get_addon_element(gManagerWindow, "theme1@tests.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, true, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled theme item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ var el = get_addon_element(gManagerWindow, "theme2@tests.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, false, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on disabled theme item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+
+add_test(function() {
+ LightweightThemeManager.currentTheme = gLWTheme;
+
+ var el = get_addon_element(gManagerWindow, "4@personas.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, true, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled LW theme item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+
+add_test(function() {
+ LightweightThemeManager.currentTheme = null;
+
+ var el = get_addon_element(gManagerWindow, "4@personas.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, false, false, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on disabled LW theme item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+});
+
+
+add_test(function() {
+ LightweightThemeManager.currentTheme = gLWTheme;
+
+ gManagerWindow.loadView("addons://detail/4@personas.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, true, false, true, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled LW theme, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ LightweightThemeManager.currentTheme = null;
+
+ gManagerWindow.loadView("addons://detail/4@personas.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, false, false, true, false);
+
+ gContextMenu.hidePopup();
+
+ AddonManager.getAddonByID("4@personas.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+ run_next_test();
+ });
+ }, false);
+
+ info("Opening context menu on disabled LW theme, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/addon1@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, true, false, true, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled extension, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/addon2@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, false, false, true, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on disabled extension, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/theme1@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, true, false, true, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on enabled theme, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/theme2@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, false, false, true, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on disabled theme, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/theme3@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(true, true, false, true, true);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu with single menu item on enabled theme, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
+
+add_test(function() {
+ info("Searching for remote addons");
+
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = SEARCH_QUERY;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var filter = gManagerWindow.document.getElementById("search-filter-remote");
+ EventUtils.synthesizeMouseAtCenter(filter, { }, gManagerWindow);
+ executeSoon(function() {
+
+ var el = get_addon_element(gManagerWindow, "remote1@tests.mozilla.org");
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, false, true, false, false);
+
+ gContextMenu.hidePopup();
+ run_next_test();
+ }, false);
+
+ info("Opening context menu on remote extension item");
+ el.parentNode.ensureElementIsVisible(el);
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+
+ });
+ });
+});
+
+
+add_test(function() {
+ gManagerWindow.loadView("addons://detail/remote1@tests.mozilla.org");
+ wait_for_view_load(gManagerWindow, function() {
+
+ gContextMenu.addEventListener("popupshown", function() {
+ gContextMenu.removeEventListener("popupshown", arguments.callee, false);
+
+ check_contextmenu(false, false, true, true, false);
+
+ gContextMenu.hidePopup();
+
+ // Delete the created install
+ AddonManager.getAllInstalls(function(aInstalls) {
+ is(aInstalls.length, 1, "Should be one available install");
+ aInstalls[0].cancel();
+
+ run_next_test();
+ });
+ }, false);
+
+ info("Opening context menu on remote extension, in detail view");
+ var el = gManagerWindow.document.querySelector("#detail-view .detail-view-container");
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug591465.xml b/toolkit/mozapps/extensions/test/browser/browser_bug591465.xml
new file mode 100644
index 000000000..9c2e102e7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug591465.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>MAGICAL SEARCH RESULT</name>
+ <type id='1'>Extension</type>
+ <guid>remote1@tests.mozilla.org</guid>
+ <version>3.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary - SEARCH SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="2">http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug591663.js b/toolkit/mozapps/extensions/test/browser/browser_bug591663.js
new file mode 100644
index 000000000..4a4f735af
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug591663.js
@@ -0,0 +1,161 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that the empty notice in the list view disappears as it should
+
+// Don't use a standard list view (e.g. "extension") to ensure that the list is
+// initially empty. Don't need to worry about the list of categories displayed
+// since only the list view itself is tested.
+var VIEW_ID = "addons://list/mock-addon";
+
+var LIST_ID = "addon-list";
+var EMPTY_ID = "addon-list-empty";
+
+var gManagerWindow;
+var gProvider;
+var gItem;
+
+var gInstallProperties = {
+ name: "Bug 591663 Mock Install",
+ type: "mock-addon"
+};
+var gAddonProperties = {
+ id: "test1@tests.mozilla.org",
+ name: "Bug 591663 Mock Add-on",
+ type: "mock-addon"
+};
+var gExtensionProperties = {
+ name: "Bug 591663 Extension Install",
+ type: "extension"
+};
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider(true, [{
+ id: "mock-addon",
+ name: "Mock Add-ons",
+ uiPriority: 4500,
+ flags: AddonManager.TYPE_UI_VIEW_LIST
+ }]);
+
+ open_manager(VIEW_ID, function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+/**
+ * Check that the list view is as expected
+ *
+ * @param aItem
+ * The expected item in the list, or null if list should be empty
+ */
+function check_list(aItem) {
+ // Check state of the empty notice
+ let emptyNotice = gManagerWindow.document.getElementById(EMPTY_ID);
+ ok(emptyNotice != null, "Should have found the empty notice");
+ is(!emptyNotice.hidden, (aItem == null), "Empty notice should be showing if list empty");
+
+ // Check the children of the list
+ let list = gManagerWindow.document.getElementById(LIST_ID);
+ is(list.childNodes.length, aItem ? 1 : 0, "Should get expected number of items in list");
+ if (aItem != null) {
+ let itemName = list.firstChild.getAttribute("name");
+ is(itemName, aItem.name, "List item should have correct name");
+ }
+}
+
+
+// Test that the empty notice is showing and no items are showing in list
+add_test(function() {
+ check_list(null);
+ run_next_test();
+});
+
+// Test that a new, non-active, install does not affect the list view
+add_test(function() {
+ gItem = gProvider.createInstalls([gInstallProperties])[0];
+ check_list(null);
+ run_next_test();
+});
+
+// Test that onInstallStarted properly hides empty notice and adds install to list
+add_test(function() {
+ gItem.addTestListener({
+ onDownloadStarted: function() {
+ // Install type unknown until download complete
+ check_list(null);
+ },
+ onInstallStarted: function() {
+ check_list(gItem);
+ },
+ onInstallEnded: function() {
+ check_list(gItem);
+ run_next_test();
+ }
+ });
+
+ gItem.install();
+});
+
+// Test that restarting the manager does not change list
+add_test(function() {
+ restart_manager(gManagerWindow, VIEW_ID, function(aManagerWindow) {
+ gManagerWindow = aManagerWindow;
+ check_list(gItem);
+ run_next_test();
+ });
+});
+
+// Test that onInstallCancelled removes install and shows empty notice
+add_test(function() {
+ gItem.cancel();
+ gItem = null;
+ check_list(null);
+ run_next_test();
+});
+
+// Test that add-ons of a different type do not show up in the list view
+add_test(function() {
+ let extension = gProvider.createInstalls([gExtensionProperties])[0];
+ check_list(null);
+
+ extension.addTestListener({
+ onDownloadStarted: function() {
+ check_list(null);
+ },
+ onInstallStarted: function() {
+ check_list(null);
+ },
+ onInstallEnded: function() {
+ check_list(null);
+ extension.cancel();
+ run_next_test();
+ }
+ });
+
+ extension.install();
+});
+
+// Test that onExternalInstall properly hides empty notice and adds install to list
+add_test(function() {
+ gItem = gProvider.createAddons([gAddonProperties])[0];
+ check_list(gItem);
+ run_next_test();
+});
+
+// Test that restarting the manager does not change list
+add_test(function() {
+ restart_manager(gManagerWindow, VIEW_ID, function(aManagerWindow) {
+ gManagerWindow = aManagerWindow;
+ check_list(gItem);
+ run_next_test();
+ });
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug593535.js b/toolkit/mozapps/extensions/test/browser/browser_bug593535.js
new file mode 100644
index 000000000..fd23d0036
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug593535.js
@@ -0,0 +1,119 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 593535 - Failure to download extension causes about:addons to list the
+// addon with no way to restart the download
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_bug593535.xml";
+const QUERY = "NOTFOUND";
+
+var gProvider;
+var gManagerWindow;
+
+function test() {
+ waitForExplicitFinish();
+
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ AddonManager.getAllInstalls(function(aInstallsList) {
+ for (var install of aInstallsList) {
+ var sourceURI = install.sourceURI.spec;
+ if (sourceURI.match(/^http:\/\/example\.com\/(.+)\.xpi$/) != null)
+ install.cancel();
+ }
+
+ finish();
+ });
+ });
+}
+
+function search(aQuery, aCallback) {
+ // Point search to the correct xml test file
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = aQuery;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var remoteFilter = gManagerWindow.document.getElementById("search-filter-remote");
+ EventUtils.synthesizeMouseAtCenter(remoteFilter, { }, gManagerWindow);
+
+ aCallback();
+ });
+}
+
+function get_addon_item(aName) {
+ var id = aName + "@tests.mozilla.org";
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+ for (let row of rows) {
+ if (row.mAddon && row.mAddon.id == id)
+ return row;
+ }
+
+ return null;
+}
+
+function get_install_button(aItem) {
+ isnot(aItem, null, "Item should not be null when checking state of install button");
+ var installStatus = getAnonymousElementByAttribute(aItem, "anonid", "install-status");
+ return getAnonymousElementByAttribute(installStatus, "anonid", "install-remote-btn");
+}
+
+
+function getAnonymousElementByAttribute(aElement, aName, aValue) {
+ return gManagerWindow.document.getAnonymousElementByAttribute(aElement,
+ aName,
+ aValue);
+}
+
+
+
+// Tests that a failed install for a remote add-on will ask to retry the install
+add_test(function() {
+ var remoteItem;
+
+ var listener = {
+ onDownloadFailed: function(aInstall) {
+ aInstall.removeListener(this);
+ ok(true, "Install failed as expected");
+
+ executeSoon(function() {
+ is(remoteItem.getAttribute("notification"), "warning", "Item should have notification attribute set to 'warning'");
+ is_element_visible(remoteItem._warning, "Warning text should be visible");
+ is(remoteItem._warning.textContent, "There was an error downloading NOTFOUND.", "Warning should show correct message");
+ is_element_visible(remoteItem._warningLink, "Retry button should be visible");
+ run_next_test();
+ });
+ },
+
+ onInstallEnded: function() {
+ ok(false, "Install should have failed");
+ }
+ }
+
+ search(QUERY, function() {
+ var list = gManagerWindow.document.getElementById("search-list");
+ remoteItem = get_addon_item("notfound1");
+ list.ensureElementIsVisible(remoteItem);
+
+ remoteItem.mAddon.install.addListener(listener);
+
+ var installBtn = get_install_button(remoteItem);
+ EventUtils.synthesizeMouseAtCenter(installBtn, { }, gManagerWindow);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug593535.xml b/toolkit/mozapps/extensions/test/browser/browser_bug593535.xml
new file mode 100644
index 000000000..411d9f383
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug593535.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>NOTFOUND</name>
+ <type id='1'>Extension</type>
+ <guid>notfound1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Install file not found - NOTFOUND</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/file_not_found.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug596336.js b/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
new file mode 100644
index 000000000..ec32e376f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug596336.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that upgrading bootstrapped add-ons behaves correctly while the
+// manager is open
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+add_task(function* test() {
+ waitForExplicitFinish();
+
+ gManagerWindow = yield open_manager("addons://list/extension");
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
+
+function get_list_item_count() {
+ return get_test_items_in_list(gManagerWindow).length;
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function get_class_node(parent, cls) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "class", cls);
+}
+
+function install_addon(aXpi) {
+ return new Promise(resolve => {
+ AddonManager.getInstallForURL(TESTROOT + "addons/" + aXpi + ".xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(aInstall) {
+ resolve();
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+ });
+}
+
+var check_addon = Task.async(function*(aAddon, aVersion) {
+ is(get_list_item_count(), 1, "Should be one item in the list");
+ is(aAddon.version, aVersion, "Add-on should have the right version");
+
+ let item = get_addon_element(gManagerWindow, "bug596336-1@tests.mozilla.org");
+ ok(!!item, "Should see the add-on in the list");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ let { version } = yield get_tooltip_info(item);
+ is(version, aVersion, "Version should be correct");
+
+ if (aAddon.userDisabled)
+ is_element_visible(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
+ else
+ is_element_hidden(get_class_node(item, "disabled-postfix"), "Disabled postfix should be hidden");
+});
+
+// Install version 1 then upgrade to version 2 with the manager open
+add_task(function*() {
+ yield install_addon("browser_bug596336_1");
+ let [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "1.0");
+ ok(!aAddon.userDisabled, "Add-on should not be disabled");
+
+ yield install_addon("browser_bug596336_2");
+ [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "2.0");
+ ok(!aAddon.userDisabled, "Add-on should not be disabled");
+
+ aAddon.uninstall();
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+});
+
+// Install version 1 mark it as disabled then upgrade to version 2 with the
+// manager open
+add_task(function*() {
+ yield install_addon("browser_bug596336_1");
+ let [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ aAddon.userDisabled = true;
+ yield check_addon(aAddon, "1.0");
+ ok(aAddon.userDisabled, "Add-on should be disabled");
+
+ yield install_addon("browser_bug596336_2");
+ [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "2.0");
+ ok(aAddon.userDisabled, "Add-on should be disabled");
+
+ aAddon.uninstall();
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+});
+
+// Install version 1 click the remove button and then upgrade to version 2 with
+// the manager open
+add_task(function*() {
+ yield install_addon("browser_bug596336_1");
+ let [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "1.0");
+ ok(!aAddon.userDisabled, "Add-on should not be disabled");
+
+ let item = get_addon_element(gManagerWindow, "bug596336-1@tests.mozilla.org");
+ EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+ is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
+
+ yield install_addon("browser_bug596336_2");
+ [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "2.0");
+ ok(!aAddon.userDisabled, "Add-on should not be disabled");
+
+ aAddon.uninstall();
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+});
+
+// Install version 1, disable it, click the remove button and then upgrade to
+// version 2 with the manager open
+add_task(function*() {
+ yield install_addon("browser_bug596336_1");
+ let [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ aAddon.userDisabled = true;
+ yield check_addon(aAddon, "1.0");
+ ok(aAddon.userDisabled, "Add-on should be disabled");
+
+ let item = get_addon_element(gManagerWindow, "bug596336-1@tests.mozilla.org");
+ EventUtils.synthesizeMouseAtCenter(get_node(item, "remove-btn"), { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+ is_element_visible(get_class_node(item, "pending"), "Pending message should be visible");
+
+ yield install_addon("browser_bug596336_2");
+ [aAddon] = yield promiseAddonsByIDs(["bug596336-1@tests.mozilla.org"]);
+ yield check_addon(aAddon, "2.0");
+ ok(aAddon.userDisabled, "Add-on should be disabled");
+
+ aAddon.uninstall();
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+});
+
+add_task(function end_test() {
+ close_manager(gManagerWindow, finish);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug608316.js b/toolkit/mozapps/extensions/test/browser/browser_bug608316.js
new file mode 100644
index 000000000..72bb61f49
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug608316.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 608316 - Test that cancelling an uninstall during the onUninstalling
+// event doesn't confuse the UI
+
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "addon 1",
+ version: "1.0"
+ }]);
+
+ run_next_test();
+}
+
+
+function end_test() {
+ finish();
+}
+
+
+add_test(function() {
+ var sawUninstall = false;
+ var listener = {
+ onUninstalling: function(aAddon, aRestartRequired) {
+ if (aAddon.id != "addon1@tests.mozilla.org")
+ return;
+ sawUninstall = true;
+ aAddon.cancelUninstall();
+ }
+ }
+
+ // Important to add this before opening the UI so it gets its events first
+ AddonManager.addAddonListener(listener);
+ registerCleanupFunction(function() {
+ AddonManager.removeAddonListener(listener);
+ });
+
+ open_manager("addons://list/extension", function(aManager) {
+ var addon = get_addon_element(aManager, "addon1@tests.mozilla.org");
+ isnot(addon, null, "Should see the add-on in the list");
+
+ var removeBtn = aManager.document.getAnonymousElementByAttribute(addon, "anonid", "remove-btn");
+ EventUtils.synthesizeMouseAtCenter(removeBtn, { }, aManager);
+
+ ok(sawUninstall, "Should have seen the uninstall event");
+ sawUninstall = false;
+
+ is(addon.getAttribute("pending"), "", "Add-on should not be uninstalling");
+
+ close_manager(aManager, function() {
+ ok(!sawUninstall, "Should not have seen another uninstall event");
+
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug610764.js b/toolkit/mozapps/extensions/test/browser/browser_bug610764.js
new file mode 100644
index 000000000..58de88130
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug610764.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the discovery view is the default
+
+var gCategoryUtilities;
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager(null, function(aWindow) {
+ waitForFocus(function() {
+ // The last view is cached except when it is the search view so switch to
+ // that and reopen to ensure we see the default view
+ var searchBox = aWindow.document.getElementById("header-search");
+ searchBox.value = "bar";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, aWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, aWindow);
+
+ wait_for_view_load(aWindow, function() {
+ close_manager(aWindow, function() {
+ open_manager(null, function(aWindow) {
+ gCategoryUtilities = new CategoryUtilities(aWindow);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should show the discovery pane by default");
+
+ close_manager(aWindow, finish);
+ });
+ });
+ });
+ }, aWindow);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug616841.js b/toolkit/mozapps/extensions/test/browser/browser_bug616841.js
new file mode 100644
index 000000000..3cf6f5346
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug616841.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function test_string_compare() {
+ ok("C".localeCompare("D") < 0, "C < D");
+ ok("D".localeCompare("C") > 0, "D > C");
+ ok("\u010C".localeCompare("D") < 0, "\u010C < D");
+ ok("D".localeCompare("\u010C") > 0, "D > \u010C");
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ test_string_compare();
+
+ AddonManager.getAddonByID("foo", function(aAddon) {
+ test_string_compare();
+ finish();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug618502.js b/toolkit/mozapps/extensions/test/browser/browser_bug618502.js
new file mode 100644
index 000000000..5bcc6baf1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug618502.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 608316 - Test that opening the manager to an add-on that doesn't exist
+// just loads the default view
+
+var gCategoryUtilities;
+
+function test() {
+ waitForExplicitFinish();
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+add_test(function() {
+ open_manager("addons://detail/foo", function(aManager) {
+ gCategoryUtilities = new CategoryUtilities(aManager);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should fall back to the discovery pane");
+
+ close_manager(aManager, run_next_test);
+ });
+});
+
+// Also test that opening directly to an add-on that does exist doesn't break
+// and selects the right category
+add_test(function() {
+ new MockProvider().createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "addon 1",
+ version: "1.0"
+ }]);
+
+ open_manager("addons://detail/addon1@tests.mozilla.org", function(aManager) {
+ gCategoryUtilities = new CategoryUtilities(aManager);
+ is(gCategoryUtilities.selectedCategory, "extension", "Should have selected the right category");
+
+ close_manager(aManager, run_next_test);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug679604.js b/toolkit/mozapps/extensions/test/browser/browser_bug679604.js
new file mode 100644
index 000000000..e1ec605c2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug679604.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 679604 - Test that a XUL persisted category from an older version of
+// Firefox doesn't break the add-ons manager when that category doesn't exist
+
+var gManagerWindow;
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager(null, function(aWindow) {
+ var categories = aWindow.document.getElementById("categories");
+ categories.setAttribute("last-selected", "foo");
+ aWindow.document.persist("categories", "last-selected");
+
+ close_manager(aWindow, function() {
+ Services.prefs.clearUserPref(PREF_UI_LASTCATEGORY);
+
+ open_manager(null, function(aWindow) {
+ is(new CategoryUtilities(aWindow).selectedCategory, "discover",
+ "Should have loaded the right view");
+
+ close_manager(aWindow, finish);
+ });
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_bug714593.js b/toolkit/mozapps/extensions/test/browser/browser_bug714593.js
new file mode 100644
index 000000000..b9a7faa5e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_bug714593.js
@@ -0,0 +1,140 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that installed addons in the search view load inline prefs properly
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const NO_MATCH_URL = TESTROOT + "browser_searching_empty.xml";
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+function test() {
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "inlinesettings2@tests.mozilla.org",
+ name: "Inline Settings (Regular)",
+ version: "1",
+ optionsURL: CHROMEROOT + "options.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+/*
+ * Checks whether or not the Add-ons Manager is currently searching
+ *
+ * @param aExpectedSearching
+ * The expected isSearching state
+ */
+function check_is_searching(aExpectedSearching) {
+ var loading = gManagerWindow.document.getElementById("search-loading");
+ is(!is_hidden(loading), aExpectedSearching,
+ "Search throbber should be showing iff currently searching");
+}
+
+/*
+ * Completes a search
+ *
+ * @param aQuery
+ * The query to search for
+ * @param aFinishImmediately
+ * Boolean representing whether or not the search is expected to
+ * finish immediately
+ * @param aCallback
+ * The callback to call when the search is done
+ * @param aCategoryType
+ * The expected selected category after the search is done.
+ * Optional and defaults to "search"
+ */
+function search(aQuery, aFinishImmediately, aCallback, aCategoryType) {
+ // Point search to the correct xml test file
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, NO_MATCH_URL);
+
+ aCategoryType = aCategoryType ? aCategoryType : "search";
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = aQuery;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ var finishImmediately = true;
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, aCategoryType, "Expected category view should be selected");
+ is(gCategoryUtilities.isTypeVisible("search"), aCategoryType == "search",
+ "Search category should only be visible if it is the current view");
+ is(finishImmediately, aFinishImmediately, "Search should finish immediately only if expected");
+
+ aCallback();
+ });
+
+ finishImmediately = false
+ if (!aFinishImmediately)
+ check_is_searching(true);
+}
+
+/*
+ * Get item for a specific add-on by name
+ *
+ * @param aName
+ * The name of the add-on to search for
+ * @return Row of add-on if found, null otherwise
+ */
+function get_addon_item(aName) {
+ var id = aName + "@tests.mozilla.org";
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+ for (let row of rows) {
+ if (row.mAddon && row.mAddon.id == id)
+ return row;
+ }
+
+ return null;
+}
+
+add_test(function() {
+ search("settings", false, function() {
+ var localFilter = gManagerWindow.document.getElementById("search-filter-local");
+ EventUtils.synthesizeMouseAtCenter(localFilter, { }, gManagerWindow);
+
+ var item = get_addon_item("inlinesettings2");
+ // Force the XBL binding to apply.
+ item.clientTop;
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gManagerWindow.gViewController.currentViewObj, gManagerWindow.gDetailView, "View should have changed to detail");
+
+ var searchCategory = gManagerWindow.document.getElementById("category-search");
+ EventUtils.synthesizeMouseAtCenter(searchCategory, { }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gManagerWindow.gViewController.currentViewObj, gManagerWindow.gSearchView, "View should have changed back to search");
+
+ // Reset filter to remote to avoid breaking later tests.
+ var remoteFilter = gManagerWindow.document.getElementById("search-filter-remote");
+ EventUtils.synthesizeMouseAtCenter(remoteFilter, { }, gManagerWindow);
+ run_next_test();
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js b/toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js
new file mode 100644
index 000000000..e7c672212
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_cancelCompatCheck.js
@@ -0,0 +1,462 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we can cancel the add-on compatibility check while it is
+// in progress (bug 772484).
+// Test framework copied from browser_bug557956.js
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_MIN_PLATFORM_COMPAT = "extensions.minCompatiblePlatformVersion";
+const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate";
+
+var repo = {};
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm", repo);
+Components.utils.import("resource://gre/modules/Promise.jsm", this);
+
+/**
+ * Test add-ons:
+ *
+ * Addon minVersion maxVersion Notes
+ * addon1 0 *
+ * addon2 0 0
+ * addon3 0 0
+ * addon4 1 *
+ * addon5 0 0 Made compatible by update check
+ * addon6 0 0 Made compatible by update check
+ * addon7 0 0 Has a broken update available
+ * addon8 0 0 Has an update available
+ * addon9 0 0 Has an update available
+ * addon10 0 0 Made incompatible by override check
+ */
+
+// describe the addons
+var ao1 = { file: "browser_bug557956_1", id: "bug557956-1@tests.mozilla.org"};
+var ao2 = { file: "browser_bug557956_2", id: "bug557956-2@tests.mozilla.org"};
+var ao3 = { file: "browser_bug557956_3", id: "bug557956-3@tests.mozilla.org"};
+var ao4 = { file: "browser_bug557956_4", id: "bug557956-4@tests.mozilla.org"};
+var ao5 = { file: "browser_bug557956_5", id: "bug557956-5@tests.mozilla.org"};
+var ao6 = { file: "browser_bug557956_6", id: "bug557956-6@tests.mozilla.org"};
+var ao7 = { file: "browser_bug557956_7", id: "bug557956-7@tests.mozilla.org"};
+var ao8 = { file: "browser_bug557956_8_1", id: "bug557956-8@tests.mozilla.org"};
+var ao9 = { file: "browser_bug557956_9_1", id: "bug557956-9@tests.mozilla.org"};
+var ao10 = { file: "browser_bug557956_10", id: "bug557956-10@tests.mozilla.org"};
+
+// Return a promise that resolves after the specified delay in MS
+function delayMS(aDelay) {
+ let deferred = Promise.defer();
+ setTimeout(deferred.resolve, aDelay);
+ return deferred.promise;
+}
+
+// Return a promise that resolves when the specified observer topic is notified
+function promise_observer(aTopic) {
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function observe(aSubject, aObsTopic, aData) {
+ Services.obs.removeObserver(arguments.callee, aObsTopic);
+ deferred.resolve([aSubject, aData]);
+ }, aTopic, false);
+ return deferred.promise;
+}
+
+// Install a set of addons using a bogus update URL so that we can force
+// the compatibility update to happen later
+// @param aUpdateURL The real update URL to use after the add-ons are installed
+function promise_install_test_addons(aAddonList, aUpdateURL) {
+ info("Starting add-on installs");
+ var installs = [];
+ let deferred = Promise.defer();
+
+ // Use a blank update URL
+ Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+ for (let addon of aAddonList) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/" + addon.file + ".xpi", function(aInstall) {
+ installs.push(aInstall);
+ }, "application/x-xpinstall");
+ }
+
+ var listener = {
+ installCount: 0,
+
+ onInstallEnded: function() {
+ this.installCount++;
+ if (this.installCount == installs.length) {
+ info("Done add-on installs");
+ // Switch to the test update URL
+ Services.prefs.setCharPref(PREF_UPDATEURL, aUpdateURL);
+ deferred.resolve();
+ }
+ }
+ };
+
+ for (let install of installs) {
+ install.addListener(listener);
+ install.install();
+ }
+
+ return deferred.promise;
+}
+
+function promise_addons_by_ids(aAddonIDs) {
+ info("promise_addons_by_ids " + aAddonIDs.toSource());
+ let deferred = Promise.defer();
+ AddonManager.getAddonsByIDs(aAddonIDs, deferred.resolve);
+ return deferred.promise;
+}
+
+function* promise_uninstall_test_addons() {
+ info("Starting add-on uninstalls");
+ let addons = yield promise_addons_by_ids([ao1.id, ao2.id, ao3.id, ao4.id, ao5.id,
+ ao6.id, ao7.id, ao8.id, ao9.id, ao10.id]);
+ let deferred = Promise.defer();
+ let uninstallCount = addons.length;
+ let listener = {
+ onUninstalled: function(aAddon) {
+ if (aAddon) {
+ info("Finished uninstalling " + aAddon.id);
+ }
+ if (--uninstallCount == 0) {
+ info("Done add-on uninstalls");
+ AddonManager.removeAddonListener(listener);
+ deferred.resolve();
+ }
+ }};
+ AddonManager.addAddonListener(listener);
+ for (let addon of addons) {
+ if (addon)
+ addon.uninstall();
+ else
+ listener.onUninstalled(null);
+ }
+ yield deferred.promise;
+}
+
+// Returns promise{window}, resolves with a handle to the compatibility
+// check window
+function promise_open_compatibility_window(aInactiveAddonIds) {
+ let deferred = Promise.defer();
+ // This will reset the longer timeout multiplier to 2 which will give each
+ // test that calls open_compatibility_window a minimum of 60 seconds to
+ // complete.
+ requestLongerTimeout(2);
+
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(aInactiveAddonIds);
+
+ // Cannot be modal as we want to interract with it, shouldn't cause problems
+ // with testing though.
+ var features = "chrome,centerscreen,dialog,titlebar";
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+ win.addEventListener("load", function() {
+ function page_shown(aEvent) {
+ if (aEvent.target.pageid)
+ info("Page " + aEvent.target.pageid + " shown");
+ }
+
+ win.removeEventListener("load", arguments.callee, false);
+
+ info("Compatibility dialog opened");
+
+ win.addEventListener("pageshow", page_shown, false);
+ win.addEventListener("unload", function() {
+ win.removeEventListener("unload", arguments.callee, false);
+ win.removeEventListener("pageshow", page_shown, false);
+ dump("Compatibility dialog closed\n");
+ }, false);
+
+ deferred.resolve(win);
+ }, false);
+ return deferred.promise;
+}
+
+function promise_window_close(aWindow) {
+ let deferred = Promise.defer();
+ aWindow.addEventListener("unload", function() {
+ aWindow.removeEventListener("unload", arguments.callee, false);
+ deferred.resolve(aWindow);
+ }, false);
+ return deferred.promise;
+}
+
+function promise_page(aWindow, aPageId) {
+ let deferred = Promise.defer();
+ var page = aWindow.document.getElementById(aPageId);
+ if (aWindow.document.getElementById("updateWizard").currentPage === page) {
+ deferred.resolve(aWindow);
+ } else {
+ page.addEventListener("pageshow", function() {
+ page.removeEventListener("pageshow", arguments.callee, false);
+ executeSoon(function() {
+ deferred.resolve(aWindow);
+ });
+ }, false);
+ }
+ return deferred.promise;
+}
+
+function get_list_names(aList) {
+ var items = [];
+ for (let listItem of aList.childNodes)
+ items.push(listItem.label);
+ items.sort();
+ return items;
+}
+
+// These add-ons became inactive during the upgrade
+var inactiveAddonIds = [
+ ao5.id,
+ ao6.id,
+ ao7.id,
+ ao8.id,
+ ao9.id
+];
+
+// Make sure the addons in the list are not installed
+function* check_addons_uninstalled(aAddonList) {
+ let foundList = yield promise_addons_by_ids(aAddonList.map(a => a.id));
+ for (let i = 0; i < aAddonList.length; i++) {
+ ok(!foundList[i], "Addon " + aAddonList[i].id + " is not installed");
+ }
+ info("Add-on uninstall check complete");
+ yield true;
+}
+
+// Test what happens when the user cancels during AddonRepository.repopulateCache()
+// Add-ons that have updates available should not update if they were disabled before
+// For this test, addon8 became disabled during update and addon9 was previously disabled,
+// so addon8 should update and addon9 should not
+add_task(function* cancel_during_repopulate() {
+ let a5, a8, a9, a10;
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+ Services.prefs.setCharPref(PREF_UPDATEURL, TESTROOT + "missing.rdf");
+
+ let installsDone = promise_observer("TEST:all-updates-done");
+
+ // Don't pull compatibility data during add-on install
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ // Set up our test addons so that the server-side JS has a 500ms delay to make
+ // sure we cancel the dialog before we get the data we want to refill our
+ // AddonRepository cache
+ let addonList = [ao5, ao8, ao9, ao10];
+ yield promise_install_test_addons(addonList,
+ TESTROOT + "cancelCompatCheck.sjs?500");
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, TESTROOT + "browser_bug557956.xml");
+
+ [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+ ok(!a5.isCompatible, "addon5 should not be compatible");
+ ok(!a8.isCompatible, "addon8 should not be compatible");
+ ok(!a9.isCompatible, "addon9 should not be compatible");
+
+ let compatWindow = yield promise_open_compatibility_window([ao5.id, ao8.id]);
+ var doc = compatWindow.document;
+ yield promise_page(compatWindow, "versioninfo");
+
+ // Brief delay to let the update window finish requesting all add-ons and start
+ // reloading the addon repository
+ yield delayMS(50);
+
+ info("Cancel the compatibility check dialog");
+ var button = doc.documentElement.getButton("cancel");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+ info("Waiting for installs to complete");
+ yield installsDone;
+ ok(!repo.AddonRepository.isSearching, "Background installs are done");
+
+ // There should be no active updates
+ let getInstalls = Promise.defer();
+ AddonManager.getAllInstalls(getInstalls.resolve);
+ let installs = yield getInstalls.promise;
+ is (installs.length, 0, "There should be no active installs after background installs are done");
+
+ // addon8 should have updated in the background,
+ // addon9 was listed as previously disabled so it should not have updated
+ [a5, a8, a9, a10] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id, ao10.id]);
+ ok(a5.isCompatible, "addon5 should be compatible");
+ ok(a8.isCompatible, "addon8 should have been upgraded");
+ ok(!a9.isCompatible, "addon9 should not have been upgraded");
+ ok(!a10.isCompatible, "addon10 should not be compatible");
+
+ info("Updates done");
+ yield promise_uninstall_test_addons();
+ info("done uninstalling add-ons");
+});
+
+// User cancels after repopulateCache, while we're waiting for the addon.findUpdates()
+// calls in gVersionInfoPage_onPageShow() to complete
+// For this test, both addon8 and addon9 were disabled by this update, but addon8
+// is set to not auto-update, so only addon9 should update in the background
+add_task(function* cancel_during_findUpdates() {
+ let a5, a8, a9;
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+ // Clear the AddonRepository-last-updated preference to ensure that it reloads
+ Services.prefs.clearUserPref(PREF_METADATA_LASTUPDATE);
+ let observeUpdateDone = promise_observer("TEST:addon-repository-data-updated");
+ let installsDone = promise_observer("TEST:all-updates-done");
+
+ // Don't pull compatibility data during add-on install
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ // No delay on the .sjs this time because we want the cache to repopulate
+ let addonList = [ao3, ao5, ao6, ao7, ao8, ao9];
+ yield promise_install_test_addons(addonList,
+ TESTROOT + "cancelCompatCheck.sjs");
+
+ [a8] = yield promise_addons_by_ids([ao8.id]);
+ a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+ var doc = compatWindow.document;
+ yield promise_page(compatWindow, "versioninfo");
+
+ info("Waiting for repository-data-updated");
+ yield observeUpdateDone;
+
+ // Quick wait to make sure the findUpdates calls get queued
+ yield delayMS(5);
+
+ info("Cancel the compatibility check dialog");
+ var button = doc.documentElement.getButton("cancel");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+ info("Waiting for installs to complete 2");
+ yield installsDone;
+ ok(!repo.AddonRepository.isSearching, "Background installs are done 2");
+
+ // addon8 should have updated in the background,
+ // addon9 was listed as previously disabled so it should not have updated
+ [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+ ok(a5.isCompatible, "addon5 should be compatible");
+ ok(!a8.isCompatible, "addon8 should not have been upgraded");
+ ok(a9.isCompatible, "addon9 should have been upgraded");
+
+ let getInstalls = Promise.defer();
+ AddonManager.getAllInstalls(getInstalls.resolve);
+ let installs = yield getInstalls.promise;
+ is (installs.length, 0, "There should be no active installs after the dialog is cancelled 2");
+
+ info("findUpdates done");
+ yield promise_uninstall_test_addons();
+});
+
+// Cancelling during the 'mismatch' screen allows add-ons that can auto-update
+// to continue updating in the background and cancels any other updates
+// Same conditions as the previous test - addon8 and addon9 have updates available,
+// addon8 is set to not auto-update so only addon9 should become compatible
+add_task(function* cancel_mismatch() {
+ let a3, a5, a7, a8, a9;
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+ // Clear the AddonRepository-last-updated preference to ensure that it reloads
+ Services.prefs.clearUserPref(PREF_METADATA_LASTUPDATE);
+ let installsDone = promise_observer("TEST:all-updates-done");
+
+ // Don't pull compatibility data during add-on install
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ // No delay on the .sjs this time because we want the cache to repopulate
+ let addonList = [ao3, ao5, ao6, ao7, ao8, ao9];
+ yield promise_install_test_addons(addonList,
+ TESTROOT + "cancelCompatCheck.sjs");
+
+ [a8] = yield promise_addons_by_ids([ao8.id]);
+ a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ // Check that the addons start out not compatible.
+ [a3, a7, a8, a9] = yield promise_addons_by_ids([ao3.id, ao7.id, ao8.id, ao9.id]);
+ ok(!a3.isCompatible, "addon3 should not be compatible");
+ ok(!a7.isCompatible, "addon7 should not be compatible");
+ ok(!a8.isCompatible, "addon8 should not be compatible");
+ ok(!a9.isCompatible, "addon9 should not be compatible");
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ let compatWindow = yield promise_open_compatibility_window(inactiveAddonIds);
+ var doc = compatWindow.document;
+ info("Wait for mismatch page");
+ yield promise_page(compatWindow, "mismatch");
+ info("Click the Don't Check button");
+ var button = doc.documentElement.getButton("cancel");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+ yield promise_window_close(compatWindow);
+ info("Waiting for installs to complete in cancel_mismatch");
+ yield installsDone;
+
+ // addon8 should not have updated in the background,
+ // addon9 was listed as previously disabled so it should not have updated
+ [a5, a8, a9] = yield promise_addons_by_ids([ao5.id, ao8.id, ao9.id]);
+ ok(a5.isCompatible, "addon5 should be compatible");
+ ok(!a8.isCompatible, "addon8 should not have been upgraded");
+ ok(a9.isCompatible, "addon9 should have been upgraded");
+
+ // Make sure there are no pending addon installs
+ let pInstalls = Promise.defer();
+ AddonManager.getAllInstalls(pInstalls.resolve);
+ let installs = yield pInstalls.promise;
+ ok(installs.length == 0, "No remaining add-on installs (" + installs.toSource() + ")");
+
+ yield promise_uninstall_test_addons();
+ yield check_addons_uninstalled(addonList);
+});
+
+// Cancelling during the 'mismatch' screen with only add-ons that have
+// no updates available
+add_task(function* cancel_mismatch_no_updates() {
+ let a3, a5, a6
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ Services.prefs.setCharPref(PREF_MIN_PLATFORM_COMPAT, "0");
+
+ // Don't pull compatibility data during add-on install
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ // No delay on the .sjs this time because we want the cache to repopulate
+ let addonList = [ao3, ao5, ao6];
+ yield promise_install_test_addons(addonList,
+ TESTROOT + "cancelCompatCheck.sjs");
+
+ // Check that the addons start out not compatible.
+ [a3, a5, a6] = yield promise_addons_by_ids([ao3.id, ao5.id, ao6.id]);
+ ok(!a3.isCompatible, "addon3 should not be compatible");
+ ok(!a5.isCompatible, "addon5 should not be compatible");
+ ok(!a6.isCompatible, "addon6 should not be compatible");
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ let compatWindow = yield promise_open_compatibility_window([ao3.id, ao5.id, ao6.id]);
+ var doc = compatWindow.document;
+ info("Wait for mismatch page");
+ yield promise_page(compatWindow, "mismatch");
+ info("Click the Don't Check button");
+ var button = doc.documentElement.getButton("cancel");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, compatWindow);
+
+ yield promise_window_close(compatWindow);
+
+ [a3, a5, a6] = yield promise_addons_by_ids([ao3.id, ao5.id, ao6.id]);
+ ok(!a3.isCompatible, "addon3 should not be compatible");
+ ok(a5.isCompatible, "addon5 should have become compatible");
+ ok(a6.isCompatible, "addon6 should have become compatible");
+
+ // Make sure there are no pending addon installs
+ let pInstalls = Promise.defer();
+ AddonManager.getAllInstalls(pInstalls.resolve);
+ let installs = yield pInstalls.promise;
+ ok(installs.length == 0, "No remaining add-on installs (" + installs.toSource() + ")");
+
+ yield promise_uninstall_test_addons();
+ yield check_addons_uninstalled(addonList);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
new file mode 100644
index 000000000..6c42e0126
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_checkAddonCompatibility.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that all bundled add-ons are compatible.
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ ok(AddonManager.strictCompatibility, "Strict compatibility should be enabled");
+
+ AddonManager.getAllAddons(function gAACallback(aAddons) {
+ // Sort add-ons (by type and name) to improve output.
+ aAddons.sort(function compareTypeName(a, b) {
+ return a.type.localeCompare(b.type) || a.name.localeCompare(b.name);
+ });
+
+ let allCompatible = true;
+ for (let a of aAddons) {
+ // Ignore plugins.
+ if (a.type == "plugin")
+ continue;
+
+ ok(a.isCompatible, a.type + " " + a.name + " " + a.version + " should be compatible");
+ allCompatible = allCompatible && a.isCompatible;
+ }
+ // Add a reminder.
+ if (!allCompatible)
+ ok(false, "As this test failed, test browser_bug557956.js should have failed, too.");
+
+ finish();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_details.js b/toolkit/mozapps/extensions/test/browser/browser_details.js
new file mode 100644
index 000000000..ce4c51b5a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_details.js
@@ -0,0 +1,1053 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests various aspects of the details view
+
+const { REQUIRE_SIGNING } = Components.utils.import("resource://gre/modules/addons/AddonConstants.jsm", {});
+
+const PREF_AUTOUPDATE_DEFAULT = "extensions.update.autoUpdateDefault";
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_details.xml";
+const PREF_EM_HOTFIX_ID = "extensions.hotfix.id";
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gVersion = Services.appinfo.version;
+var gDate = new Date(2010, 7, 1);
+var infoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+
+function open_details(aId, aType, aCallback) {
+ requestLongerTimeout(2);
+
+ gCategoryUtilities.openType(aType, function() {
+ var list = gManagerWindow.document.getElementById("addon-list");
+ var item = list.firstChild;
+ while (item) {
+ if ("mAddon" in item && item.mAddon.id == aId) {
+ list.ensureElementIsVisible(item);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, aCallback);
+ return;
+ }
+ item = item.nextSibling;
+ }
+ ok(false, "Should have found the add-on in the list");
+ });
+}
+
+function get(aId) {
+ return gManagerWindow.document.getElementById(aId);
+}
+
+function test() {
+ requestLongerTimeout(2);
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_ID, "hotfix@tests.mozilla.org");
+
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ version: "2.1",
+ description: "Short description",
+ fullDescription: "Longer description",
+ type: "extension",
+ iconURL: "chrome://foo/skin/icon.png",
+ icon64URL: "chrome://foo/skin/icon64.png",
+ contributionURL: "http://foo.com",
+ contributionAmount: "$0.99",
+ sourceURI: Services.io.newURI("http://example.com/foo", null, null),
+ averageRating: 4,
+ reviewCount: 5,
+ reviewURL: "http://example.com/reviews",
+ homepageURL: "http://example.com/addon1",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "2.2",
+ description: "Short description",
+ creator: { name: "Mozilla", url: null },
+ type: "extension",
+ iconURL: "chrome://foo/skin/icon.png",
+ contributionURL: "http://foo.com",
+ contributionAmount: null,
+ updateDate: gDate,
+ permissions: 0,
+ screenshots: [{
+ url: "chrome://branding/content/about.png",
+ width: 200,
+ height: 150
+ }],
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ description: "Short description",
+ creator: { name: "Mozilla", url: "http://www.mozilla.org" },
+ type: "extension",
+ sourceURI: Services.io.newURI("http://example.com/foo", null, null),
+ updateDate: gDate,
+ reviewCount: 1,
+ reviewURL: "http://example.com/reviews",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE,
+ isActive: false,
+ isCompatible: false,
+ appDisabled: true,
+ permissions: AddonManager.PERM_CAN_ENABLE |
+ AddonManager.PERM_CAN_DISABLE |
+ AddonManager.PERM_CAN_UPGRADE,
+ screenshots: [{
+ url: "http://example.com/screenshot",
+ width: 400,
+ height: 300,
+ thumbnailURL: "chrome://branding/content/icon64.png",
+ thumbnailWidth: 160,
+ thumbnailHeight: 120
+ }],
+ }, {
+ id: "addon4@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon4@tests.mozilla.org",
+ name: "Test add-on 4",
+ _userDisabled: true,
+ isActive: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_SOFTBLOCKED
+ }, {
+ id: "addon5@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon5@tests.mozilla.org",
+ name: "Test add-on 5",
+ isActive: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_BLOCKED,
+ appDisabled: true
+ }, {
+ id: "addon6@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon6@tests.mozilla.org",
+ name: "Test add-on 6",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon7@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon7@tests.mozilla.org",
+ name: "Test add-on 7",
+ _userDisabled: true,
+ isActive: false
+ }, {
+ id: "addon8@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon8@tests.mozilla.org",
+ name: "Test add-on 8",
+ blocklistState: Ci.nsIBlocklistService.STATE_OUTDATED
+ }, {
+ id: "addon9@tests.mozilla.org",
+ name: "Test add-on 9",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ }, {
+ id: "addon10@tests.mozilla.org",
+ name: "Test add-on 10",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ isActive: false,
+ appDisabled: true,
+ isCompatible: false,
+ }, {
+ id: "addon11@tests.mozilla.org",
+ name: "Test add-on 11",
+ signedState: AddonManager.SIGNEDSTATE_PRELIMINARY,
+ foreignInstall: true,
+ isActive: false,
+ appDisabled: true,
+ isCompatible: false,
+ }, {
+ id: "addon12@tests.mozilla.org",
+ name: "Test add-on 12",
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ foreignInstall: true,
+ }, {
+ id: "hotfix@tests.mozilla.org",
+ name: "Test hotfix 1",
+ }]);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+}
+
+function end_test() {
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_ID);
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+// Opens and tests the details view for add-on 1
+add_test(function() {
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 1", "Name should be correct");
+ is_element_visible(get("detail-version"), "Version should not be hidden");
+ is(get("detail-version").value, "2.1", "Version should be correct");
+ is(get("detail-icon").src, "chrome://foo/skin/icon64.png", "Icon should be correct");
+ is_element_hidden(get("detail-creator"), "Creator should be hidden");
+ is_element_hidden(get("detail-screenshot-box"), "Screenshot should be hidden");
+ is(get("detail-screenshot").width, "", "Screenshot dimensions should not be set");
+ is(get("detail-screenshot").height, "", "Screenshot dimensions should not be set");
+ is(get("detail-desc").textContent, "Short description", "Description should be correct");
+ is(get("detail-fulldesc").textContent, "Longer description", "Full description should be correct");
+
+ is_element_visible(get("detail-contributions"), "Contributions section should be visible");
+ is_element_visible(get("detail-contrib-suggested"), "Contributions amount should be visible");
+ ok(get("detail-contrib-suggested").value, "$0.99");
+
+ is_element_visible(get("detail-updates-row"), "Updates should not be hidden");
+ is_element_hidden(get("detail-dateUpdated"), "Update date should be hidden");
+
+ is_element_visible(get("detail-rating-row"), "Rating row should not be hidden");
+ is_element_visible(get("detail-rating"), "Rating should not be hidden");
+ is(get("detail-rating").averageRating, 4, "Rating should be correct");
+ is_element_visible(get("detail-reviews"), "Reviews should not be hidden");
+ is(get("detail-reviews").href, "http://example.com/reviews", "Review URL should be correct");
+ is(get("detail-reviews").value, "5 reviews", "Review text should be correct");
+
+ is_element_visible(get("detail-homepage-row"), "Homepage should be visible");
+ ok(get("detail-homepage").href, "http://example.com/addon1");
+ is_element_hidden(get("detail-repository-row"), "Repository profile should not be visible");
+
+ is_element_hidden(get("detail-size"), "Size should be hidden");
+
+ is_element_hidden(get("detail-downloads"), "Downloads should be hidden");
+
+ is_element_visible(get("detail-autoUpdate"), "Updates should not be hidden");
+ ok(get("detail-autoUpdate").childNodes[1].selected, "Updates ahould be automatic");
+ is_element_hidden(get("detail-findUpdates-btn"), "Check for updates should be hidden");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").lastChild, {}, gManagerWindow);
+ ok(get("detail-autoUpdate").lastChild.selected, "Updates should be manual");
+ is_element_visible(get("detail-findUpdates-btn"), "Check for updates should be visible");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").firstChild, {}, gManagerWindow);
+ ok(get("detail-autoUpdate").firstChild.selected, "Updates should be automatic");
+ is_element_hidden(get("detail-findUpdates-btn"), "Check for updates should be hidden");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Disable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-disable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 1 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Reopen it
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 1 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Undo disabling
+ EventUtils.synthesizeMouseAtCenter(get("detail-undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 2
+add_test(function() {
+ open_details("addon2@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 2", "Name should be correct");
+ is_element_visible(get("detail-version"), "Version should not be hidden");
+ is(get("detail-version").value, "2.2", "Version should be correct");
+ is(get("detail-icon").src, "chrome://foo/skin/icon.png", "Icon should be correct");
+
+ is_element_visible(get("detail-creator"), "Creator should not be hidden");
+ is_element_visible(get("detail-creator")._creatorName, "Creator name should not be hidden");
+ is(get("detail-creator")._creatorName.value, "Mozilla", "Creator should be correct");
+ is_element_hidden(get("detail-creator")._creatorLink, "Creator link should be hidden");
+
+ is_element_visible(get("detail-screenshot-box"), "Screenshot should be visible");
+ is(get("detail-screenshot").src, "chrome://branding/content/about.png", "Should be showing the full sized screenshot");
+ is(get("detail-screenshot").width, 200, "Screenshot dimensions should be set");
+ is(get("detail-screenshot").height, 150, "Screenshot dimensions should be set");
+ is(get("detail-screenshot").hasAttribute("loading"), true, "Screenshot should have loading attribute");
+ is(get("detail-desc").textContent, "Short description", "Description should be correct");
+ is_element_hidden(get("detail-fulldesc"), "Full description should be hidden");
+
+ is_element_visible(get("detail-contributions"), "Contributions section should be visible");
+ is_element_hidden(get("detail-contrib-suggested"), "Contributions amount should be hidden");
+
+ is_element_visible(get("detail-dateUpdated"), "Update date should not be hidden");
+ is(get("detail-dateUpdated").value, formatDate(gDate), "Update date should be correct");
+
+ is_element_hidden(get("detail-rating-row"), "Rating should be hidden");
+
+ is_element_hidden(get("detail-homepage-row"), "Homepage should not be visible");
+ is_element_hidden(get("detail-repository-row"), "Repository profile should not be visible");
+
+ is_element_hidden(get("detail-size"), "Size should be hidden");
+
+ is_element_hidden(get("detail-downloads"), "Downloads should be hidden");
+
+ is_element_hidden(get("detail-updates-row"), "Updates should be hidden");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_hidden(get("detail-uninstall-btn"), "Remove button should be hidden");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ get("detail-screenshot").addEventListener("load", function() {
+ this.removeEventListener("load", arguments.callee, false);
+ is(this.hasAttribute("loading"), false, "Screenshot should not have loading attribute");
+ run_next_test();
+ }, false);
+ });
+});
+
+// Opens and tests the details view for add-on 3
+add_test(function() {
+ open_details("addon3@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 3", "Name should be correct");
+ is_element_hidden(get("detail-version"), "Version should be hidden");
+ is(get("detail-icon").src, "", "Icon should be correct");
+
+ is_element_visible(get("detail-creator"), "Creator should not be hidden");
+ is_element_hidden(get("detail-creator")._creatorName, "Creator name should be hidden");
+ is_element_visible(get("detail-creator")._creatorLink, "Creator link should not be hidden");
+ is(get("detail-creator")._creatorLink.value, "Mozilla", "Creator link should be correct");
+ is(get("detail-creator")._creatorLink.href, "http://www.mozilla.org", "Creator link href should be correct");
+
+ is_element_visible(get("detail-screenshot-box"), "Screenshot should be visible");
+ is(get("detail-screenshot").src, "chrome://branding/content/icon64.png", "Should be showing the thumbnail");
+ is(get("detail-screenshot").width, 160, "Screenshot dimensions should be set");
+ is(get("detail-screenshot").height, 120, "Screenshot dimensions should be set");
+ is(get("detail-screenshot").hasAttribute("loading"), true, "Screenshot should have loading attribute");
+
+ is_element_hidden(get("detail-contributions"), "Contributions section should be hidden");
+
+ is_element_visible(get("detail-updates-row"), "Updates should not be hidden");
+ is_element_visible(get("detail-dateUpdated"), "Update date should not be hidden");
+ is(get("detail-dateUpdated").value, formatDate(gDate), "Update date should be correct");
+
+ is_element_visible(get("detail-rating-row"), "Rating row should not be hidden");
+ is_element_hidden(get("detail-rating"), "Rating should be hidden");
+ is_element_visible(get("detail-reviews"), "Reviews should not be hidden");
+ is(get("detail-reviews").href, "http://example.com/reviews", "Review URL should be correct");
+ is(get("detail-reviews").value, "1 review", "Review text should be correct");
+
+ is_element_hidden(get("detail-size"), "Size should be hidden");
+
+ is_element_hidden(get("detail-downloads"), "Downloads should be hidden");
+
+ is_element_visible(get("detail-autoUpdate"), "Updates should not be hidden");
+ ok(get("detail-autoUpdate").lastChild.selected, "Updates should be manual");
+ is_element_visible(get("detail-findUpdates-btn"), "Check for updates should be visible");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").childNodes[1], {}, gManagerWindow);
+ ok(get("detail-autoUpdate").childNodes[1].selected, "Updates should be automatic");
+ is_element_hidden(get("detail-findUpdates-btn"), "Check for updates should be hidden");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").lastChild, {}, gManagerWindow);
+ ok(get("detail-autoUpdate").lastChild.selected, "Updates should be manual");
+ is_element_visible(get("detail-findUpdates-btn"), "Check for updates should be visible");
+
+ info("Setting " + PREF_AUTOUPDATE_DEFAULT + " to true");
+ Services.prefs.setBoolPref(PREF_AUTOUPDATE_DEFAULT, true);
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").firstChild, {}, gManagerWindow);
+ ok(get("detail-autoUpdate").firstChild.selected, "Updates should be default");
+ is_element_hidden(get("detail-findUpdates-btn"), "Check for updates should be hidden");
+
+ info("Setting " + PREF_AUTOUPDATE_DEFAULT + " to false");
+ Services.prefs.setBoolPref(PREF_AUTOUPDATE_DEFAULT, false);
+ ok(get("detail-autoUpdate").firstChild.selected, "Updates should be default");
+ is_element_visible(get("detail-findUpdates-btn"), "Check for updates should be visible");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").childNodes[1], {}, gManagerWindow);
+ ok(get("detail-autoUpdate").childNodes[1].selected, "Updates should be automatic");
+ is_element_hidden(get("detail-findUpdates-btn"), "Check for updates should be hidden");
+ EventUtils.synthesizeMouseAtCenter(get("detail-autoUpdate").firstChild, {}, gManagerWindow);
+ ok(get("detail-autoUpdate").firstChild.selected, "Updates should be default");
+ is_element_visible(get("detail-findUpdates-btn"), "Check for updates should be visible");
+ Services.prefs.clearUserPref(PREF_AUTOUPDATE_DEFAULT);
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_hidden(get("detail-uninstall-btn"), "Remove button should be hidden");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 3 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ get("detail-screenshot").addEventListener("load", function() {
+ this.removeEventListener("load", arguments.callee, false);
+ is(this.hasAttribute("loading"), false, "Screenshot should not have loading attribute");
+ run_next_test();
+ }, false);
+ });
+});
+
+// Opens and tests the details view for add-on 4
+add_test(function() {
+ open_details("addon4@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 4", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+ is_element_visible(get("detail-warning-link"), "Warning link should be visible");
+ is(get("detail-warning-link").value, "More Information", "Warning link text should be correct");
+ is(get("detail-warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Enable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Reopen it
+ open_details("addon4@tests.mozilla.org", "extension", function() {
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Undo enabling
+ EventUtils.synthesizeMouseAtCenter(get("detail-undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+ is_element_visible(get("detail-warning-link"), "Warning link should be visible");
+ is(get("detail-warning-link").value, "More Information", "Warning link text should be correct");
+ is(get("detail-warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 5
+add_test(function() {
+ open_details("addon5@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 5", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_visible(get("detail-error"), "Error message should be visible");
+ is(get("detail-error").textContent, "Test add-on 5 has been disabled due to security or stability issues.", "Error message should be correct");
+ is_element_visible(get("detail-error-link"), "Error link should be visible");
+ is(get("detail-error-link").value, "More Information", "Error link text should be correct");
+ is(get("detail-error-link").href, "http://example.com/addon5@tests.mozilla.org", "Error link should be correct");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Opens and tests the details view for add-on 6
+add_test(function() {
+ open_details("addon6@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 6", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Disable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-disable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Reopen it
+ open_details("addon6@tests.mozilla.org", "extension", function() {
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be visible");
+
+ // Enable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 7
+add_test(function() {
+ open_details("addon7@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 7", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Enable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 7 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Reopen it
+ open_details("addon7@tests.mozilla.org", "extension", function() {
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 7 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Undo enabling
+ EventUtils.synthesizeMouseAtCenter(get("detail-undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 8
+add_test(function() {
+ open_details("addon8@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 8", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "An important update is available for Test add-on 8.", "Warning message should be correct");
+ is_element_visible(get("detail-warning-link"), "Warning link should be visible");
+ is(get("detail-warning-link").value, "Update Now", "Warning link text should be correct");
+ is(get("detail-warning-link").href, "http://example.com/addon8@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ // Disable it
+ EventUtils.synthesizeMouseAtCenter(get("detail-disable-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 8 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Reopen it
+ open_details("addon8@tests.mozilla.org", "extension", function() {
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_visible(get("detail-enable-btn"), "Enable button should be visible");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-pending"), "Pending message should be visible");
+ is(get("detail-pending").textContent, "Test add-on 8 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ // Undo disabling
+ EventUtils.synthesizeMouseAtCenter(get("detail-undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "An important update is available for Test add-on 8.", "Warning message should be correct");
+ is_element_visible(get("detail-warning-link"), "Warning link should be visible");
+ is(get("detail-warning-link").value, "Update Now", "Warning link text should be correct");
+ is(get("detail-warning-link").href, "http://example.com/addon8@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+});
+
+// These tests are only appropriate when signing can be turned off
+if (!REQUIRE_SIGNING) {
+ // Opens and tests the details view for add-on 9
+ add_test(function() {
+ open_details("addon9@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 9", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_visible(get("detail-warning"), "Error message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 9 could not be verified for use in " + gApp + ". Proceed with caution.", "Warning message should be correct");
+ is_element_visible(get("detail-warning-link"), "Warning link should be visible");
+ is(get("detail-warning-link").value, "More Information", "Warning link text should be correct");
+ is(get("detail-warning-link").href, infoURL, "Warning link should be correct");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+}
+
+// Opens and tests the details view for add-on 9 with signing required
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ open_details("addon9@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 9", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_visible(get("detail-error"), "Error message should be visible");
+ is(get("detail-error").textContent, "Test add-on 9 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
+ is_element_visible(get("detail-error-link"), "Error link should be visible");
+ is(get("detail-error-link").value, "More Information", "Error link text should be correct");
+ is(get("detail-error-link").href, infoURL, "Error link should be correct");
+
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// These tests are only appropriate when signing can be turned off
+if (!REQUIRE_SIGNING) {
+ // Opens and tests the details view for add-on 10
+ add_test(function() {
+ open_details("addon10@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 10", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 10 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+}
+
+// Opens and tests the details view for add-on 10 with signing required
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ open_details("addon10@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 10", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_visible(get("detail-error"), "Error message should be visible");
+ is(get("detail-error").textContent, "Test add-on 10 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
+ is_element_visible(get("detail-error-link"), "Error link should be visible");
+ is(get("detail-error-link").value, "More Information", "Error link text should be correct");
+ is(get("detail-error-link").href, infoURL, "Error link should be correct");
+
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 11
+add_test(function() {
+ open_details("addon11@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 11", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 11 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Opens and tests the details view for add-on 11 with signing required
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ open_details("addon11@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 11", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get("detail-disable-btn"), "Disable button should be hidden");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on 11 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Opens and tests the details view for add-on 12
+add_test(function() {
+ open_details("addon12@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 12", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Opens and tests the details view for add-on 12 with signing required
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ open_details("addon12@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test add-on 12", "Name should be correct");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-error-link"), "Error link should be hidden");
+
+ close_manager(gManagerWindow, function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Opens and tests the details view for hotfix 1
+add_test(function() {
+ open_details("hotfix@tests.mozilla.org", "extension", function() {
+ is(get("detail-name").textContent, "Test hotfix 1", "Name should be correct");
+
+ is_element_hidden(get("detail-updates-row"), "Updates should be hidden");
+
+ is_element_hidden(get("detail-prefs-btn"), "Preferences button should be hidden");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Tests that upgrades with onExternalInstall apply immediately
+add_test(function() {
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on replacement",
+ version: "2.5",
+ description: "Short description replacement",
+ fullDescription: "Longer description replacement",
+ type: "extension",
+ iconURL: "chrome://foo/skin/icon.png",
+ icon64URL: "chrome://foo/skin/icon264.png",
+ sourceURI: Services.io.newURI("http://example.com/foo", null, null),
+ averageRating: 2,
+ optionsURL: "chrome://foo/content/options.xul",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }]);
+
+ is(get("detail-name").textContent, "Test add-on replacement", "Name should be correct");
+ is_element_visible(get("detail-version"), "Version should not be hidden");
+ is(get("detail-version").value, "2.5", "Version should be correct");
+ is(get("detail-icon").src, "chrome://foo/skin/icon264.png", "Icon should be correct");
+ is_element_hidden(get("detail-creator"), "Creator should be hidden");
+ is_element_hidden(get("detail-screenshot-box"), "Screenshot should be hidden");
+ is(get("detail-desc").textContent, "Short description replacement", "Description should be correct");
+ is(get("detail-fulldesc").textContent, "Longer description replacement", "Full description should be correct");
+
+ is_element_hidden(get("detail-contributions"), "Contributions section should be hidden");
+
+ is_element_hidden(get("detail-dateUpdated"), "Update date should be hidden");
+
+ is_element_visible(get("detail-rating-row"), "Rating row should not be hidden");
+ is_element_visible(get("detail-rating"), "Rating should not be hidden");
+ is(get("detail-rating").averageRating, 2, "Rating should be correct");
+ is_element_hidden(get("detail-reviews"), "Reviews should be hidden");
+
+ is_element_hidden(get("detail-homepage-row"), "Homepage should be hidden");
+
+ is_element_hidden(get("detail-size"), "Size should be hidden");
+
+ is_element_hidden(get("detail-downloads"), "Downloads should be hidden");
+
+ is_element_visible(get("detail-prefs-btn"), "Preferences button should be visible");
+ is_element_hidden(get("detail-enable-btn"), "Enable button should be hidden");
+ is_element_visible(get("detail-disable-btn"), "Disable button should be visible");
+ is_element_visible(get("detail-uninstall-btn"), "Remove button should be visible");
+
+ is_element_hidden(get("detail-warning"), "Warning message should be hidden");
+ is_element_hidden(get("detail-warning-link"), "Warning link should be hidden");
+ is_element_hidden(get("detail-error"), "Error message should be hidden");
+ is_element_hidden(get("detail-pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Check that onPropertyChanges for appDisabled updates the UI
+add_test(function() {
+ info("Checking that onPropertyChanges for appDisabled updates the UI");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ aAddon.userDisabled = true;
+ aAddon.isCompatible = true;
+ aAddon.appDisabled = false;
+
+ open_details("addon1@tests.mozilla.org", "extension", function() {
+ is(get("detail-view").getAttribute("active"), "false", "Addon should not be marked as active");
+ is_element_hidden(get("detail-warning"), "Warning message should not be visible");
+
+ info("Making addon incompatible and appDisabled");
+ aAddon.isCompatible = false;
+ aAddon.appDisabled = true;
+
+ is(get("detail-view").getAttribute("active"), "false", "Addon should not be marked as active");
+ is_element_visible(get("detail-warning"), "Warning message should be visible");
+ is(get("detail-warning").textContent, "Test add-on replacement is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_discovery.js b/toolkit/mozapps/extensions/test/browser/browser_discovery.js
new file mode 100644
index 000000000..ec336df2d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_discovery.js
@@ -0,0 +1,651 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the discovery view loads properly
+
+const MAIN_URL = "https://example.com/" + RELATIVE_DIR + "discovery.html";
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+var gLoadCompleteCallback = null;
+
+var gProgressListener = {
+ onStateChange: function(aWebProgress, aRequest, aStateFlags, aStatus) {
+ // Only care about the network stop status events
+ if (!(aStateFlags & (Ci.nsIWebProgressListener.STATE_IS_NETWORK)) ||
+ !(aStateFlags & (Ci.nsIWebProgressListener.STATE_STOP)))
+ return;
+
+ if (gLoadCompleteCallback)
+ executeSoon(gLoadCompleteCallback);
+ gLoadCompleteCallback = null;
+ },
+
+ onLocationChange: function() { },
+ onSecurityChange: function() { },
+ onProgressChange: function() { },
+ onStatusChange: function() { },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
+ Ci.nsISupportsWeakReference]),
+};
+
+function test() {
+ // Switch to a known url
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL);
+ // Temporarily enable caching
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ type: "extension",
+ version: "2.2",
+ isCompatible: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_SOFTBLOCKED,
+ userDisabled: false
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ type: "plugin",
+ version: "3.1.5",
+ isCompatible: true,
+ blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ userDisabled: false
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ type: "theme",
+ version: "1.2b1",
+ isCompatible: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_BLOCKED,
+ userDisabled: true
+ }]);
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+function getURL(aBrowser) {
+ if (gManagerWindow.document.getElementById("discover-view").selectedPanel !=
+ aBrowser)
+ return null;
+
+ var url = aBrowser.currentURI.spec;
+ var pos = url.indexOf("#");
+ if (pos != -1)
+ return url.substring(0, pos);
+ return url;
+}
+
+function getHash(aBrowser) {
+ if (gManagerWindow.document.getElementById("discover-view").selectedPanel !=
+ aBrowser)
+ return null;
+
+ var url = aBrowser.currentURI.spec;
+ var pos = url.indexOf("#");
+ if (pos != -1)
+ return decodeURIComponent(url.substring(pos + 1));
+ return null;
+}
+
+function testHash(aBrowser, aTestAddonVisible, aCallback) {
+ var hash = getHash(aBrowser);
+ isnot(hash, null, "There should be a hash");
+ try {
+ var data = JSON.parse(hash);
+ }
+ catch (e) {
+ ok(false, "Hash should have been valid JSON: " + e);
+ aCallback();
+ return;
+ }
+ is(typeof data, "object", "Hash should be a JS object");
+
+ // Ensure that at least the test add-ons are present
+ if (aTestAddonVisible[0])
+ ok("addon1@tests.mozilla.org" in data, "Test add-on 1 should be listed");
+ else
+ ok(!("addon1@tests.mozilla.org" in data), "Test add-on 1 should not be listed");
+ if (aTestAddonVisible[1])
+ ok("addon2@tests.mozilla.org" in data, "Test add-on 2 should be listed");
+ else
+ ok(!("addon2@tests.mozilla.org" in data), "Test add-on 2 should not be listed");
+ if (aTestAddonVisible[2])
+ ok("addon3@tests.mozilla.org" in data, "Test add-on 3 should be listed");
+ else
+ ok(!("addon3@tests.mozilla.org" in data), "Test add-on 3 should not be listed");
+
+ // Test against all the add-ons the manager knows about since plugins and
+ // app extensions may exist
+ AddonManager.getAllAddons(function(aAddons) {
+ for (let addon of aAddons) {
+ if (!(addon.id in data)) {
+ // Test add-ons will have shown an error if necessary above
+ if (addon.id.substring(6) != "@tests.mozilla.org")
+ ok(false, "Add-on " + addon.id + " was not included in the data");
+ continue;
+ }
+
+ info("Testing data for add-on " + addon.id);
+ var addonData = data[addon.id];
+ is(addonData.name, addon.name, "Name should be correct");
+ is(addonData.version, addon.version, "Version should be correct");
+ is(addonData.type, addon.type, "Type should be correct");
+ is(addonData.userDisabled, addon.userDisabled, "userDisabled should be correct");
+ is(addonData.isBlocklisted, addon.blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED, "blocklisted should be correct");
+ is(addonData.isCompatible, addon.isCompatible, "isCompatible should be correct");
+ }
+ aCallback();
+ });
+}
+
+function isLoading() {
+ var loading = gManagerWindow.document.getElementById("discover-view").selectedPanel ==
+ gManagerWindow.document.getElementById("discover-loading");
+ if (loading) {
+ is_element_visible(gManagerWindow.document.querySelector("#discover-loading .loading"),
+ "Loading message should be visible when its panel is the selected panel");
+ }
+ return loading;
+}
+
+function isError() {
+ return gManagerWindow.document.getElementById("discover-view").selectedPanel ==
+ gManagerWindow.document.getElementById("discover-error");
+}
+
+function clickLink(aId, aCallback) {
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ browser.addProgressListener(gProgressListener);
+
+ gLoadCompleteCallback = function() {
+ browser.removeProgressListener(gProgressListener);
+ aCallback();
+ };
+
+ var link = browser.contentDocument.getElementById(aId);
+ EventUtils.sendMouseEvent({type: "click"}, link);
+
+ executeSoon(function() {
+ ok(isLoading(), "Clicking a link should show the loading pane");
+ });
+}
+
+// Tests that switching to the discovery view displays the right url
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ gCategoryUtilities.openType("discover", function() {
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ testHash(browser, [true, true, true], function() {
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+
+ ok(isLoading(), "Should be loading at first");
+ });
+});
+
+// Tests that loading the add-ons manager with the discovery view as the last
+// selected view displays the right url
+add_test(function() {
+ // Hide one of the test add-ons
+ Services.prefs.setBoolPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", false);
+ Services.prefs.setBoolPref("extensions.addon3@tests.mozilla.org.getAddons.cache.enabled", true);
+
+ open_manager(null, function(aWindow) {
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should have loaded the right view");
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ testHash(browser, [true, false, true], function() {
+ close_manager(gManagerWindow, run_next_test);
+ });
+ }, function(aWindow) {
+ gManagerWindow = aWindow;
+ ok(isLoading(), "Should be loading at first");
+ });
+});
+
+// Tests that loading the add-ons manager with the discovery view as the initial
+// view displays the right url
+add_test(function() {
+ Services.prefs.clearUserPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled");
+ Services.prefs.setBoolPref("extensions.addon3@tests.mozilla.org.getAddons.cache.enabled", false);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ gCategoryUtilities.openType("extension", function() {
+ close_manager(gManagerWindow, function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should have loaded the right view");
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ testHash(browser, [true, true, false], function() {
+ Services.prefs.clearUserPref("extensions.addon3@tests.mozilla.org.getAddons.cache.enabled");
+ close_manager(gManagerWindow, run_next_test);
+ });
+ }, function(aWindow) {
+ gManagerWindow = aWindow;
+ ok(isLoading(), "Should be loading at first");
+ });
+ });
+ });
+ });
+});
+
+// Tests that switching to the discovery view displays the right url
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ gCategoryUtilities.openType("discover", function() {
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ is(getHash(browser), null, "Hash should not have been passed");
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+});
+
+// Tests that loading the add-ons manager with the discovery view as the last
+// selected view displays the right url
+add_test(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should have loaded the right view");
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ is(getHash(browser), null, "Hash should not have been passed");
+ close_manager(gManagerWindow, run_next_test);
+ });
+});
+
+// Tests that loading the add-ons manager with the discovery view as the initial
+// view displays the right url
+add_test(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ gCategoryUtilities.openType("extension", function() {
+ close_manager(gManagerWindow, function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "discover", "Should have loaded the right view");
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ is(getHash(browser), null, "Hash should not have been passed");
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Tests that navigating to an insecure page fails
+add_test(function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-http", function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ ok(isLoading(), "Should start loading again");
+ });
+ });
+ });
+});
+
+// Tests that navigating to a different domain fails
+add_test(function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-domain", function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ ok(isLoading(), "Should start loading again");
+ });
+ });
+ });
+});
+
+// Tests that navigating to a missing page fails
+add_test(function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-bad", function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ ok(isLoading(), "Should start loading again");
+ });
+ });
+ });
+});
+
+// Tests that navigating to a page on the same domain works
+add_test(function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-good", function() {
+ is(getURL(browser), "https://example.com/" + RELATIVE_DIR + "releaseNotes.xhtml", "Should have loaded the right url");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Tests repeated navigation to the same page followed by a navigation to a
+// different domain
+add_test(function() {
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ var count = 10;
+ function clickAgain(aCallback) {
+ if (count-- == 0)
+ aCallback();
+ else
+ clickLink("link-normal", clickAgain.bind(null, aCallback));
+ }
+
+ clickAgain(function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-domain", function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ ok(isLoading(), "Should start loading again");
+ });
+ });
+ });
+ });
+});
+
+// Loading an insecure main page should work if that is what the prefs say, should
+// also be able to navigate to a https page and back again
+add_test(function() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, TESTROOT + "discovery.html");
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), TESTROOT + "discovery.html", "Should have loaded the right url");
+
+ clickLink("link-normal", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ clickLink("link-http", function() {
+ is(getURL(browser), TESTROOT + "discovery.html", "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+});
+
+// Stopping the initial load should display the error page and then correctly
+// reload when switching away and back again
+add_test(function() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+
+ EventUtils.synthesizeMouse(gCategoryUtilities.get("discover"), 2, 2, { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ EventUtils.synthesizeMouse(gCategoryUtilities.get("discover"), 2, 2, { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ ok(isError(), "Should have shown the error page");
+
+ gCategoryUtilities.openType("extension", function() {
+ gCategoryUtilities.openType("discover", function() {
+ is(getURL(browser), MAIN_URL, "Should have loaded the right url");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+
+ ok(isLoading(), "Should be loading");
+ // This will stop the real page load
+ browser.stop();
+ });
+ });
+
+ ok(isLoading(), "Should be loading");
+ // This will actually stop the about:blank load
+ browser.stop();
+ });
+});
+
+// Test for Bug 703929 - Loading the discover view from a chrome XUL file fails when
+// the add-on manager is reopened.
+add_test(function() {
+ const url = "chrome://mochitests/content/" + RELATIVE_DIR + "addon_about.xul";
+ Services.prefs.setCharPref(PREF_DISCOVERURL, url);
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), url, "Loading a chrome XUL file should work");
+
+ restart_manager(gManagerWindow, "addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), url, "Should be able to load the chrome XUL file a second time");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+});
+
+// Bug 711693 - Send the compatibility mode when loading the Discovery pane
+add_test(function() {
+ info("Test '%COMPATIBILITY_MODE%' in the URL is correctly replaced by 'normal'");
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL + "?mode=%COMPATIBILITY_MODE%");
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, false);
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL + "?mode=normal", "Should have loaded the right url");
+ close_manager(gManagerWindow, run_next_test);
+ });
+});
+
+add_test(function() {
+ info("Test '%COMPATIBILITY_MODE%' in the URL is correctly replaced by 'strict'");
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL + "?mode=strict", "Should have loaded the right url");
+ close_manager(gManagerWindow, run_next_test);
+ });
+});
+
+add_test(function() {
+ info("Test '%COMPATIBILITY_MODE%' in the URL is correctly replaced by 'ignore'");
+ Services.prefs.setBoolPref(PREF_CHECK_COMPATIBILITY, false);
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+ var browser = gManagerWindow.document.getElementById("discover-browser");
+ is(getURL(browser), MAIN_URL + "?mode=ignore", "Should have loaded the right url");
+ close_manager(gManagerWindow, run_next_test);
+ });
+});
+
+// Test for Bug 601442 - extensions.getAddons.showPane need to be update
+// for the new addon manager.
+function bug_601442_test_elements(visible) {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ if (visible)
+ ok(gCategoryUtilities.isTypeVisible("discover"), "Discover category should be visible");
+ else
+ ok(!gCategoryUtilities.isTypeVisible("discover"), "Discover category should not be visible");
+
+ gManagerWindow.loadView("addons://list/dictionary");
+ wait_for_view_load(gManagerWindow, function(aManager) {
+ var button = aManager.document.getElementById("discover-button-install");
+ if (visible)
+ ok(!is_hidden(button), "Discover button should be visible!");
+ else
+ ok(is_hidden(button), "Discover button should not be visible!");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+}
+
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_DISCOVER_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, true);
+ bug_601442_test_elements(false);
+});
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_DISCOVER_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, false);
+ bug_601442_test_elements(false);
+});
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_DISCOVER_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, false);
+ bug_601442_test_elements(false);
+});
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_DISCOVER_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, true);
+ bug_601442_test_elements(true);
+});
+
+// Test for Bug 1132971 - if extensions.getAddons.showPane is false,
+// the extensions pane should show by default
+add_test(function() {
+ Services.prefs.clearUserPref(PREF_UI_LASTCATEGORY);
+ Services.prefs.setBoolPref(PREF_DISCOVER_ENABLED, false);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be showing the extension view");
+ close_manager(gManagerWindow, run_next_test);
+ Services.prefs.clearUserPref(PREF_DISCOVER_ENABLED);
+ });
+});
+
+// Test for Bug 1219495 - should show placeholder content when offline
+add_test(function() {
+ // set a URL to cause an error
+ Services.prefs.setCharPref(PREF_DISCOVERURL, "https://nocert.example.com/");
+
+ open_manager("addons://discover/", function(aWindow) {
+ gManagerWindow = aWindow;
+
+ ok(isError(), "Should have shown the placeholder content");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_discovery_install.js b/toolkit/mozapps/extensions/test/browser/browser_discovery_install.js
new file mode 100644
index 000000000..63516bd53
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_discovery_install.js
@@ -0,0 +1,133 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the discovery view can install add-ons correctly
+
+const MAIN_URL = "https://example.com/" + RELATIVE_DIR + "discovery_install.html";
+const GOOD_FRAMED_URL = "https://example.com/" + RELATIVE_DIR + "discovery_frame.html";
+const BAD_FRAMED_URL = "https://example.org/" + RELATIVE_DIR + "discovery_frame.html";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+
+// Temporarily enable caching
+Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+// Allow SSL from non-built-in certs
+Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+// Allow installs from the test site
+Services.perms.add(NetUtil.newURI("https://example.com/"), "install",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+Services.perms.add(NetUtil.newURI("https://example.org/"), "install",
+ Ci.nsIPermissionManager.ALLOW_ACTION);
+
+registerCleanupFunction(() => {
+ Services.perms.remove(NetUtil.newURI("https://example.com/"), "install");
+ Services.perms.remove(NetUtil.newURI("https://example.org/"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+});
+
+function clickLink(frameLoader, id) {
+ let link = frameLoader.contentDocument.getElementById(id);
+ EventUtils.sendMouseEvent({type: "click"}, link);
+}
+
+function waitForInstall() {
+ return new Promise(resolve => {
+ wait_for_window_open((window) => {
+ is(window.location, "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul",
+ "Should have seen the install window");
+ window.document.documentElement.cancelDialog();
+ resolve();
+ });
+ });
+}
+
+function waitForFail() {
+ return new Promise(resolve => {
+ let listener = (subject, topic, data) => {
+ Services.obs.removeObserver(listener, topic);
+ resolve();
+ }
+ Services.obs.addObserver(listener, "addon-install-origin-blocked", false);
+ });
+}
+
+// Tests that navigating to an XPI attempts to install correctly
+add_task(function* test_install_direct() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+
+ clickLink(browser, "install-direct");
+ yield waitForInstall();
+
+ yield close_manager(managerWindow);
+});
+
+// Tests that installing via JS works correctly
+add_task(function* test_install_js() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, MAIN_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+
+ clickLink(browser, "install-js");
+ yield waitForInstall();
+
+ yield close_manager(managerWindow);
+});
+
+// Installing from an inner-frame of the same origin should work
+add_task(function* test_install_inner_direct() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, GOOD_FRAMED_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+ let frame = browser.contentDocument.getElementById("frame");
+
+ clickLink(frame, "install-direct");
+ yield waitForInstall();
+
+ yield close_manager(managerWindow);
+});
+
+add_task(function* test_install_inner_js() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, GOOD_FRAMED_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+ let frame = browser.contentDocument.getElementById("frame");
+
+ clickLink(frame, "install-js");
+ yield waitForInstall();
+
+ yield close_manager(managerWindow);
+});
+
+// Installing from an inner-frame of a different origin should fail
+add_task(function* test_install_xorigin_direct() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, BAD_FRAMED_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+ let frame = browser.contentDocument.getElementById("frame");
+
+ clickLink(frame, "install-direct");
+ yield waitForFail();
+
+ yield close_manager(managerWindow);
+});
+
+add_task(function* test_install_xorigin_js() {
+ Services.prefs.setCharPref(PREF_DISCOVERURL, BAD_FRAMED_URL);
+
+ let managerWindow = yield open_manager("addons://discover/");
+ let browser = managerWindow.document.getElementById("discover-browser");
+ let frame = browser.contentDocument.getElementById("frame");
+
+ clickLink(frame, "install-js");
+ yield waitForFail();
+
+ yield close_manager(managerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
new file mode 100644
index 000000000..960e7e933
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_dragdrop.js
@@ -0,0 +1,234 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests simulated drag and drop of files into the add-ons manager.
+// We test with the add-ons manager in its own tab if in Firefox otherwise
+// in its own window.
+// Tests are only simulations of the drag and drop events, we cannot really do
+// this automatically.
+
+// Instead of loading EventUtils.js into the test scope in browser-test.js for all tests,
+// we only need EventUtils.js for a few files which is why we are using loadSubScript.
+var gManagerWindow;
+var EventUtils = {};
+this._scriptLoader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+this._scriptLoader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils);
+
+// This listens for the next opened window and checks it is of the right url.
+// opencallback is called when the new window is fully loaded
+// closecallback is called when the window is closed
+function WindowOpenListener(url, opencallback, closecallback) {
+ this.url = url;
+ this.opencallback = opencallback;
+ this.closecallback = closecallback;
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ wm.addListener(this);
+}
+
+WindowOpenListener.prototype = {
+ url: null,
+ opencallback: null,
+ closecallback: null,
+ window: null,
+ domwindow: null,
+
+ handleEvent: function(event) {
+ is(this.domwindow.document.location.href, this.url, "Should have opened the correct window");
+
+ this.domwindow.removeEventListener("load", this, false);
+ // Allow any other load handlers to execute
+ var self = this;
+ executeSoon(function() { self.opencallback(self.domwindow); } );
+ },
+
+ onWindowTitleChange: function(window, title) {
+ },
+
+ onOpenWindow: function(window) {
+ if (this.window)
+ return;
+
+ this.window = window;
+ this.domwindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ this.domwindow.addEventListener("load", this, false);
+ },
+
+ onCloseWindow: function(window) {
+ if (this.window != window)
+ return;
+
+ var wm = Components.classes["@mozilla.org/appshell/window-mediator;1"]
+ .getService(Components.interfaces.nsIWindowMediator);
+ wm.removeListener(this);
+ this.opencallback = null;
+ this.window = null;
+ this.domwindow = null;
+
+ // Let the window close complete
+ executeSoon(this.closecallback);
+ this.closecallback = null;
+ }
+};
+
+var gSawInstallNotification = false;
+var gInstallNotificationObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ var installInfo = aSubject.QueryInterface(Ci.amIWebInstallInfo);
+ if (gTestInWindow)
+ is(installInfo.browser, null, "Notification should have a null browser");
+ else
+ isnot(installInfo.browser, null, "Notification should have non-null browser");
+ gSawInstallNotification = true;
+ Services.obs.removeObserver(this, "addon-install-started");
+ }
+};
+
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+function test_confirmation(aWindow, aExpectedURLs) {
+ var list = aWindow.document.getElementById("itemList");
+ is(list.childNodes.length, aExpectedURLs.length, "Should be the right number of installs");
+
+ for (let url of aExpectedURLs) {
+ let found = false;
+ for (let node of list.children) {
+ if (node.url == url) {
+ found = true;
+ break;
+ }
+ }
+ ok(found, "Should have seen " + url + " in the list");
+ }
+
+ aWindow.document.documentElement.cancelDialog();
+}
+
+// Simulates dropping a URL onto the manager
+add_test(function() {
+ var url = TESTROOT + "addons/browser_dragdrop1.xpi";
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ test_confirmation(aWindow, [url]);
+ }, function() {
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+ run_next_test();
+ });
+
+ var viewContainer = gManagerWindow.document.getElementById("view-port");
+ var effect = EventUtils.synthesizeDrop(viewContainer, viewContainer,
+ [[{type: "text/x-moz-url", data: url}]],
+ "copy", gManagerWindow);
+ is(effect, "copy", "Drag should be accepted");
+});
+
+// Simulates dropping a file onto the manager
+add_test(function() {
+ var fileurl = get_addon_file_url("browser_dragdrop1.xpi");
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ test_confirmation(aWindow, [fileurl.spec]);
+ }, function() {
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+ run_next_test();
+ });
+
+ var viewContainer = gManagerWindow.document.getElementById("view-port");
+ var effect = EventUtils.synthesizeDrop(viewContainer, viewContainer,
+ [[{type: "application/x-moz-file", data: fileurl.file}]],
+ "copy", gManagerWindow);
+ is(effect, "copy", "Drag should be accepted");
+});
+
+// Simulates dropping two urls onto the manager
+add_test(function() {
+ var url1 = TESTROOT + "addons/browser_dragdrop1.xpi";
+ var url2 = TESTROOT2 + "addons/browser_dragdrop2.xpi";
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ test_confirmation(aWindow, [url1, url2]);
+ }, function() {
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+ run_next_test();
+ });
+
+ var viewContainer = gManagerWindow.document.getElementById("view-port");
+ var effect = EventUtils.synthesizeDrop(viewContainer, viewContainer,
+ [[{type: "text/x-moz-url", data: url1}],
+ [{type: "text/x-moz-url", data: url2}]],
+ "copy", gManagerWindow);
+ is(effect, "copy", "Drag should be accepted");
+});
+
+// Simulates dropping two files onto the manager
+add_test(function() {
+ var fileurl1 = get_addon_file_url("browser_dragdrop1.xpi");
+ var fileurl2 = get_addon_file_url("browser_dragdrop2.xpi");
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ test_confirmation(aWindow, [fileurl1.spec, fileurl2.spec]);
+ }, function() {
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+ run_next_test();
+ });
+
+ var viewContainer = gManagerWindow.document.getElementById("view-port");
+ var effect = EventUtils.synthesizeDrop(viewContainer, viewContainer,
+ [[{type: "application/x-moz-file", data: fileurl1.file}],
+ [{type: "application/x-moz-file", data: fileurl2.file}]],
+ "copy", gManagerWindow);
+ is(effect, "copy", "Drag should be accepted");
+});
+
+// Simulates dropping a file and a url onto the manager (weird, but should still work)
+add_test(function() {
+ var url = TESTROOT + "addons/browser_dragdrop1.xpi";
+ var fileurl = get_addon_file_url("browser_dragdrop2.xpi");
+
+ Services.obs.addObserver(gInstallNotificationObserver,
+ "addon-install-started", false);
+
+ new WindowOpenListener(INSTALL_URI, function(aWindow) {
+ test_confirmation(aWindow, [url, fileurl.spec]);
+ }, function() {
+ is(gSawInstallNotification, true, "Should have seen addon-install-started notification.");
+ run_next_test();
+ });
+
+ var viewContainer = gManagerWindow.document.getElementById("view-port");
+ var effect = EventUtils.synthesizeDrop(viewContainer, viewContainer,
+ [[{type: "text/x-moz-url", data: url}],
+ [{type: "application/x-moz-file", data: fileurl.file}]],
+ "copy", gManagerWindow);
+ is(effect, "copy", "Drag should be accepted");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_eula.js b/toolkit/mozapps/extensions/test/browser/browser_eula.js
new file mode 100644
index 000000000..befe9f1f2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_eula.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the eula is shown correctly for search results
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gSearchCount = 0;
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+ Services.prefs.setCharPref("extensions.getAddons.search.url", TESTROOT + "browser_eula.xml");
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, finish);
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function installSearchResult(aCallback) {
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ // Search for something different each time
+ searchBox.value = "foo" + gSearchCount;
+ gSearchCount++;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ let remote = gManagerWindow.document.getElementById("search-filter-remote")
+ EventUtils.synthesizeMouseAtCenter(remote, { }, gManagerWindow);
+
+ let item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ ok(!!item, "Should see the search result in the list");
+
+ let status = get_node(item, "install-status");
+ EventUtils.synthesizeMouseAtCenter(get_node(status, "install-remote-btn"), {}, gManagerWindow);
+
+ item.mInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ });
+}
+
+// Install an add-on through the search page, accept the EULA and then undo it
+add_test(function() {
+ // Accept the EULA when it appears
+ let sawEULA = false;
+ wait_for_window_open(function(aWindow) {
+ sawEULA = true;
+ is(aWindow.location.href, "chrome://mozapps/content/extensions/eula.xul", "Window opened should be correct");
+ is(aWindow.document.getElementById("eula").value, "This is the EULA for this add-on", "EULA should be correct");
+
+ aWindow.document.documentElement.acceptDialog();
+ });
+
+ installSearchResult(function() {
+ ok(sawEULA, "Should have seen the EULA");
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ is(aInstalls.length, 1, "Should be one pending install");
+ aInstalls[0].cancel();
+
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_eula.xml b/toolkit/mozapps/extensions/test/browser/browser_eula.xml
new file mode 100644
index 000000000..965ab8a0b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_eula.xml
@@ -0,0 +1,35 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1">
+ <addon>
+ <name>Install Tests</name>
+ <type id='1'>Extension</type>
+ <guid>addon1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test add-on</summary>
+ <description>Test add-on</description>
+ <eula>This is the EULA for this add-on</eula>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="2">http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_experiments.js b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
new file mode 100644
index 000000000..18a548de5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_experiments.js
@@ -0,0 +1,654 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Promise.jsm", this);
+
+var {AddonManagerTesting} = Components.utils.import("resource://testing-common/AddonManagerTesting.jsm", {});
+var {HttpServer} = Components.utils.import("resource://testing-common/httpd.js", {});
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gExperiments;
+var gHttpServer;
+
+var gSavedManifestURI;
+var gIsEnUsLocale;
+
+const SEC_IN_ONE_DAY = 24 * 60 * 60;
+const MS_IN_ONE_DAY = SEC_IN_ONE_DAY * 1000;
+
+function getExperimentAddons() {
+ let deferred = Promise.defer();
+ AddonManager.getAddonsByTypes(["experiment"], (addons) => {
+ deferred.resolve(addons);
+ });
+ return deferred.promise;
+}
+
+function getInstallItem() {
+ let doc = gManagerWindow.document;
+ let view = get_current_view(gManagerWindow);
+ let list = doc.getElementById("addon-list");
+
+ let node = list.firstChild;
+ while (node) {
+ if (node.getAttribute("status") == "installing") {
+ return node;
+ }
+ node = node.nextSibling;
+ }
+
+ return null;
+}
+
+function patchPolicy(policy, data) {
+ for (let key of Object.keys(data)) {
+ Object.defineProperty(policy, key, {
+ value: data[key],
+ writable: true,
+ });
+ }
+}
+
+function defineNow(policy, time) {
+ patchPolicy(policy, { now: () => new Date(time) });
+}
+
+function openDetailsView(aId) {
+ let item = get_addon_element(gManagerWindow, aId);
+ Assert.ok(item, "Should have got add-on element.");
+ is_element_visible(item, "Add-on element should be visible.");
+
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+
+ let deferred = Promise.defer();
+ wait_for_view_load(gManagerWindow, deferred.resolve);
+ return deferred.promise;
+}
+
+function clickRemoveButton(addonElement) {
+ let btn = gManagerWindow.document.getAnonymousElementByAttribute(addonElement, "anonid", "remove-btn");
+ if (!btn) {
+ return Promise.reject();
+ }
+
+ EventUtils.synthesizeMouseAtCenter(btn, { clickCount: 1 }, gManagerWindow);
+ let deferred = Promise.defer();
+ setTimeout(deferred.resolve, 0);
+ return deferred;
+}
+
+function clickUndoButton(addonElement) {
+ let btn = gManagerWindow.document.getAnonymousElementByAttribute(addonElement, "anonid", "undo-btn");
+ if (!btn) {
+ return Promise.reject();
+ }
+
+ EventUtils.synthesizeMouseAtCenter(btn, { clickCount: 1 }, gManagerWindow);
+ let deferred = Promise.defer();
+ setTimeout(deferred.resolve, 0);
+ return deferred;
+}
+
+add_task(function* initializeState() {
+ gManagerWindow = yield open_manager();
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("experiments.enabled");
+ Services.prefs.clearUserPref("toolkit.telemetry.enabled");
+ if (gHttpServer) {
+ gHttpServer.stop(() => {});
+ if (gSavedManifestURI !== undefined) {
+ Services.prefs.setCharPref("experments.manifest.uri", gSavedManifestURI);
+ }
+ }
+ if (gExperiments) {
+ let tmp = {};
+ Cu.import("resource:///modules/experiments/Experiments.jsm", tmp);
+ gExperiments._policy = new tmp.Experiments.Policy();
+ }
+ });
+
+ let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+ gIsEnUsLocale = chrome.getSelectedLocale("global") == "en-US";
+
+ // The Experiments Manager will interfere with us by preventing installs
+ // of experiments it doesn't know about. We remove it from the equation
+ // because here we are only concerned with core Addon Manager operation,
+ // not the superset Experiments Manager has imposed.
+ if ("@mozilla.org/browser/experiments-service;1" in Components.classes) {
+ let tmp = {};
+ Cu.import("resource:///modules/experiments/Experiments.jsm", tmp);
+ // There is a race condition between XPCOM service initialization and
+ // this test running. We have to initialize the instance first, then
+ // uninitialize it to prevent this.
+ gExperiments = tmp.Experiments.instance();
+ yield gExperiments._mainTask;
+ yield gExperiments.uninit();
+ }
+});
+
+// On an empty profile with no experiments, the experiment category
+// should be hidden.
+add_task(function* testInitialState() {
+ Assert.ok(gCategoryUtilities.get("experiment", false), "Experiment tab is defined.");
+ Assert.ok(!gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab hidden by default.");
+});
+
+add_task(function* testExperimentInfoNotVisible() {
+ yield gCategoryUtilities.openType("extension");
+ let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+ is_element_hidden(el, "Experiment info not visible on other types.");
+});
+
+// If we have an active experiment, we should see the experiments tab
+// and that tab should have some messages.
+add_task(function* testActiveExperiment() {
+ let addon = yield install_addon("addons/browser_experiment1.xpi");
+
+ Assert.ok(addon.userDisabled, "Add-on is disabled upon initial install.");
+ Assert.equal(addon.isActive, false, "Add-on is not active.");
+
+ Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
+
+ yield gCategoryUtilities.openType("experiment");
+ let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+ is_element_visible(el, "Experiment info is visible on experiment tab.");
+});
+
+add_task(function* testExperimentLearnMore() {
+ // Actual URL is irrelevant.
+ Services.prefs.setCharPref("toolkit.telemetry.infoURL",
+ "http://mochi.test:8888/server.js");
+
+ yield gCategoryUtilities.openType("experiment");
+ let btn = gManagerWindow.document.getElementById("experiments-learn-more");
+
+ if (!gUseInContentUI) {
+ is_element_hidden(btn, "Learn more button hidden if not using in-content UI.");
+ Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
+
+ return;
+ }
+
+ is_element_visible(btn, "Learn more button visible.");
+
+ let deferred = Promise.defer();
+ window.addEventListener("DOMContentLoaded", function onLoad(event) {
+ info("Telemetry privacy policy window opened.");
+ window.removeEventListener("DOMContentLoaded", onLoad, false);
+
+ let browser = gBrowser.selectedBrowser;
+ let expected = Services.prefs.getCharPref("toolkit.telemetry.infoURL");
+ Assert.equal(browser.currentURI.spec, expected, "New tab should have loaded privacy policy.");
+ browser.contentWindow.close();
+
+ Services.prefs.clearUserPref("toolkit.telemetry.infoURL");
+
+ deferred.resolve();
+ }, false);
+
+ info("Opening telemetry privacy policy.");
+ EventUtils.synthesizeMouseAtCenter(btn, {}, gManagerWindow);
+
+ yield deferred.promise;
+});
+
+add_task(function* testOpenPreferences() {
+ yield gCategoryUtilities.openType("experiment");
+ let btn = gManagerWindow.document.getElementById("experiments-change-telemetry");
+ if (!gUseInContentUI) {
+ is_element_hidden(btn, "Change telemetry button not enabled in out of window UI.");
+ info("Skipping preferences open test because not using in-content UI.");
+ return;
+ }
+
+ is_element_visible(btn, "Change telemetry button visible in in-content UI.");
+
+ let deferred = Promise.defer();
+ Services.obs.addObserver(function observer(prefWin, topic, data) {
+ Services.obs.removeObserver(observer, "advanced-pane-loaded");
+ info("Advanced preference pane opened.");
+ executeSoon(function() {
+ // We want this test to fail if the preferences pane changes.
+ let el = prefWin.document.getElementById("dataChoicesPanel");
+ is_element_visible(el);
+
+ prefWin.close();
+ info("Closed preferences pane.");
+
+ deferred.resolve();
+ });
+ }, "advanced-pane-loaded", false);
+
+ info("Loading preferences pane.");
+ // We need to focus before synthesizing the mouse event (bug 1240052) as
+ // synthesizeMouseAtCenter currently only synthesizes the mouse in the child process.
+ // This can cause some subtle differences if the child isn't focused.
+ yield SimpleTest.promiseFocus();
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#experiments-change-telemetry", {},
+ gBrowser.selectedBrowser);
+
+ yield deferred.promise;
+});
+
+add_task(function* testButtonPresence() {
+ yield gCategoryUtilities.openType("experiment");
+ let item = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ // Corresponds to the uninstall permission.
+ is_element_visible(el, "Remove button is visible.");
+ // Corresponds to lack of disable permission.
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
+ is_element_hidden(el, "Disable button not visible.");
+ // Corresponds to lack of enable permission.
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
+ is_element_hidden(el, "Enable button not visible.");
+});
+
+// Remove the add-on we've been testing with.
+add_task(function* testCleanup() {
+ yield AddonManagerTesting.uninstallAddonByID("test-experiment1@experiments.mozilla.org");
+ // Verify some conditions, just in case.
+ let addons = yield getExperimentAddons();
+ Assert.equal(addons.length, 0, "No experiment add-ons are installed.");
+});
+
+// The following tests should ideally live in browser/experiments/. However,
+// they rely on some of the helper functions from head.js, which can't easily
+// be consumed from other directories. So, they live here.
+
+add_task(function* testActivateExperiment() {
+ if (!gExperiments) {
+ info("Skipping experiments test because that feature isn't available.");
+ return;
+ }
+
+ gHttpServer = new HttpServer();
+ gHttpServer.start(-1);
+ let root = "http://localhost:" + gHttpServer.identity.primaryPort + "/";
+ gHttpServer.registerPathHandler("/manifest", (request, response) => {
+ response.setStatusLine(null, 200, "OK");
+ response.write(JSON.stringify({
+ "version": 1,
+ "experiments": [
+ {
+ id: "experiment-1",
+ xpiURL: TESTROOT + "addons/browser_experiment1.xpi",
+ xpiHash: "IRRELEVANT",
+ startTime: Date.now() / 1000 - 3600,
+ endTime: Date.now() / 1000 + 3600,
+ maxActiveSeconds: 600,
+ appName: [Services.appinfo.name],
+ channel: [gExperiments._policy.updatechannel()],
+ },
+ ],
+ }));
+ response.processAsync();
+ response.finish();
+ });
+
+ gSavedManifestURI = Services.prefs.getCharPref("experiments.manifest.uri");
+ Services.prefs.setCharPref("experiments.manifest.uri", root + "manifest");
+
+ // We need to remove the cache file to help ensure consistent state.
+ yield OS.File.remove(gExperiments._cacheFilePath);
+
+ Services.prefs.setBoolPref("toolkit.telemetry.enabled", true);
+ Services.prefs.setBoolPref("experiments.enabled", true);
+
+ info("Initializing experiments service.");
+ yield gExperiments.init();
+ info("Experiments service finished first run.");
+
+ // Check conditions, just to be sure.
+ let experiments = yield gExperiments.getExperiments();
+ Assert.equal(experiments.length, 0, "No experiments known to the service.");
+
+ // This makes testing easier.
+ gExperiments._policy.ignoreHashes = true;
+
+ info("Manually updating experiments manifest.");
+ yield gExperiments.updateManifest();
+ info("Experiments update complete.");
+
+ let deferred = Promise.defer();
+ gHttpServer.stop(() => {
+ gHttpServer = null;
+
+ info("getting experiment by ID");
+ AddonManager.getAddonByID("test-experiment1@experiments.mozilla.org", (addon) => {
+ Assert.ok(addon, "Add-on installed via Experiments manager.");
+
+ deferred.resolve();
+ });
+ });
+
+ yield deferred.promise;
+
+ Assert.ok(gCategoryUtilities.isTypeVisible, "experiment", "Experiment tab visible.");
+ yield gCategoryUtilities.openType("experiment");
+ let el = gManagerWindow.document.getElementsByClassName("experiment-info-container")[0];
+ is_element_visible(el, "Experiment info is visible on experiment tab.");
+});
+
+add_task(function* testDeactivateExperiment() {
+ if (!gExperiments) {
+ return;
+ }
+
+ // Fake an empty manifest to purge data from previous manifest.
+ yield gExperiments._updateExperiments({
+ "version": 1,
+ "experiments": [],
+ });
+
+ yield gExperiments.disableExperiment("testing");
+
+ // We should have a record of the previously-active experiment.
+ let experiments = yield gExperiments.getExperiments();
+ Assert.equal(experiments.length, 1, "1 experiment is known.");
+ Assert.equal(experiments[0].active, false, "Experiment is not active.");
+
+ // We should have a previous experiment in the add-ons manager.
+ let deferred = Promise.defer();
+ AddonManager.getAddonsByTypes(["experiment"], (addons) => {
+ deferred.resolve(addons);
+ });
+ let addons = yield deferred.promise;
+ Assert.equal(addons.length, 1, "1 experiment add-on known.");
+ Assert.ok(addons[0].appDisabled, "It is a previous experiment.");
+ Assert.equal(addons[0].id, "experiment-1", "Add-on ID matches expected.");
+
+ // Verify the UI looks sane.
+
+ Assert.ok(gCategoryUtilities.isTypeVisible("experiment"), "Experiment tab visible.");
+ let item = get_addon_element(gManagerWindow, "experiment-1");
+ Assert.ok(item, "Got add-on element.");
+ Assert.ok(!item.active, "Element should not be active.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ // User control buttons should not be present because previous experiments
+ // should have no permissions.
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ is_element_hidden(el, "Remove button is not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
+ is_element_hidden(el, "Disable button is not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
+ is_element_hidden(el, "Enable button is not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "preferences-btn");
+ is_element_hidden(el, "Preferences button is not visible.");
+});
+
+add_task(function* testActivateRealExperiments() {
+ if (!gExperiments) {
+ info("Skipping experiments test because that feature isn't available.");
+ return;
+ }
+
+ yield gExperiments._updateExperiments({
+ "version": 1,
+ "experiments": [
+ {
+ id: "experiment-2",
+ xpiURL: TESTROOT + "addons/browser_experiment1.xpi",
+ xpiHash: "IRRELEVANT",
+ startTime: Date.now() / 1000 - 3600,
+ endTime: Date.now() / 1000 + 3600,
+ maxActiveSeconds: 600,
+ appName: [Services.appinfo.name],
+ channel: [gExperiments._policy.updatechannel()],
+ },
+ ],
+ });
+ yield gExperiments._run();
+
+ // Check the active experiment.
+
+ let item = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Active");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Less than a day remaining");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "error-container");
+ is_element_hidden(el, "error-container should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning-container");
+ is_element_hidden(el, "warning-container should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
+ is_element_hidden(el, "pending-container should be hidden.");
+ let { version } = yield get_tooltip_info(item);
+ Assert.equal(version, undefined, "version should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
+ is_element_hidden(el, "disabled-postfix should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");
+ is_element_hidden(el, "update-postfix should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "experiment-bullet");
+ is_element_visible(el, "experiment-bullet should be visible.");
+
+ // Check the previous experiment.
+
+ item = get_addon_element(gManagerWindow, "experiment-1");
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Complete");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Less than a day ago");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "error-container");
+ is_element_hidden(el, "error-container should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning-container");
+ is_element_hidden(el, "warning-container should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "pending-container");
+ is_element_hidden(el, "pending-container should be hidden.");
+ ({ version } = yield get_tooltip_info(item));
+ Assert.equal(version, undefined, "version should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
+ is_element_hidden(el, "disabled-postfix should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "update-postfix");
+ is_element_hidden(el, "update-postfix should be hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "experiment-bullet");
+ is_element_visible(el, "experiment-bullet should be visible.");
+
+ // Install an "older" experiment.
+
+ yield gExperiments.disableExperiment("experiment-2");
+
+ let now = Date.now();
+ let fakeNow = now - 5 * MS_IN_ONE_DAY;
+ defineNow(gExperiments._policy, fakeNow);
+
+ yield gExperiments._updateExperiments({
+ "version": 1,
+ "experiments": [
+ {
+ id: "experiment-3",
+ xpiURL: TESTROOT + "addons/browser_experiment1.xpi",
+ xpiHash: "IRRELEVANT",
+ startTime: fakeNow / 1000 - SEC_IN_ONE_DAY,
+ endTime: now / 1000 + 10 * SEC_IN_ONE_DAY,
+ maxActiveSeconds: 100 * SEC_IN_ONE_DAY,
+ appName: [Services.appinfo.name],
+ channel: [gExperiments._policy.updatechannel()],
+ },
+ ],
+ });
+ yield gExperiments._run();
+
+ // Check the active experiment.
+
+ item = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Active");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "10 days remaining");
+ }
+
+ // Disable it and check it's previous experiment entry.
+
+ yield gExperiments.disableExperiment("experiment-3");
+
+ item = get_addon_element(gManagerWindow, "experiment-3");
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Complete");
+ }
+
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "5 days ago");
+ }
+});
+
+add_task(function* testDetailView() {
+ if (!gExperiments) {
+ info("Skipping experiments test because that feature isn't available.");
+ return;
+ }
+
+ defineNow(gExperiments._policy, Date.now());
+ yield gExperiments._updateExperiments({
+ "version": 1,
+ "experiments": [
+ {
+ id: "experiment-4",
+ xpiURL: TESTROOT + "addons/browser_experiment1.xpi",
+ xpiHash: "IRRELEVANT",
+ startTime: Date.now() / 1000 - 3600,
+ endTime: Date.now() / 1000 + 3600,
+ maxActiveSeconds: 600,
+ appName: [Services.appinfo.name],
+ channel: [gExperiments._policy.updatechannel()],
+ },
+ ],
+ });
+ yield gExperiments._run();
+
+ // Check active experiment.
+
+ yield openDetailsView("test-experiment1@experiments.mozilla.org");
+
+ let el = gManagerWindow.document.getElementById("detail-experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Active");
+ }
+
+ el = gManagerWindow.document.getElementById("detail-experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Less than a day remaining");
+ }
+
+ el = gManagerWindow.document.getElementById("detail-version");
+ is_element_hidden(el, "detail-version should be hidden.");
+ el = gManagerWindow.document.getElementById("detail-creator");
+ is_element_hidden(el, "detail-creator should be hidden.");
+ el = gManagerWindow.document.getElementById("detail-experiment-bullet");
+ is_element_visible(el, "experiment-bullet should be visible.");
+
+ // Check previous experiment.
+
+ yield gCategoryUtilities.openType("experiment");
+ yield openDetailsView("experiment-3");
+
+ el = gManagerWindow.document.getElementById("detail-experiment-state");
+ is_element_visible(el, "Experiment state label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "Complete");
+ }
+
+ el = gManagerWindow.document.getElementById("detail-experiment-time");
+ is_element_visible(el, "Experiment time label should be visible.");
+ if (gIsEnUsLocale) {
+ Assert.equal(el.value, "5 days ago");
+ }
+
+ el = gManagerWindow.document.getElementById("detail-version");
+ is_element_hidden(el, "detail-version should be hidden.");
+ el = gManagerWindow.document.getElementById("detail-creator");
+ is_element_hidden(el, "detail-creator should be hidden.");
+ el = gManagerWindow.document.getElementById("detail-experiment-bullet");
+ is_element_visible(el, "experiment-bullet should be visible.");
+});
+
+add_task(function* testRemoveAndUndo() {
+ if (!gExperiments) {
+ info("Skipping experiments test because that feature isn't available.");
+ return;
+ }
+
+ yield gCategoryUtilities.openType("experiment");
+
+ let addon = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
+ Assert.ok(addon, "Got add-on element.");
+
+ yield clickRemoveButton(addon);
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ let el = gManagerWindow.document.getAnonymousElementByAttribute(addon, "class", "pending");
+ is_element_visible(el, "Uninstall undo information should be visible.");
+
+ yield clickUndoButton(addon);
+ addon = get_addon_element(gManagerWindow, "test-experiment1@experiments.mozilla.org");
+ Assert.ok(addon, "Got add-on element.");
+});
+
+add_task(function* testCleanup() {
+ if (gExperiments) {
+ Services.prefs.clearUserPref("experiments.enabled");
+ Services.prefs.setCharPref("experiments.manifest.uri", gSavedManifestURI);
+
+ // We perform the uninit/init cycle to purge any leftover state.
+ yield OS.File.remove(gExperiments._cacheFilePath);
+ yield gExperiments.uninit();
+ yield gExperiments.init();
+
+ Services.prefs.clearUserPref("toolkit.telemetry.enabled");
+ }
+
+ // Check post-conditions.
+ let addons = yield getExperimentAddons();
+ Assert.equal(addons.length, 0, "No experiment add-ons are installed.");
+
+ yield close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js
new file mode 100644
index 000000000..663905a90
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_globalwarnings.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Bug 566194 - safe mode / security & compatibility check status are not exposed in new addon manager UI
+
+function test() {
+ waitForExplicitFinish();
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+add_test(function() {
+ info("Testing compatibility checking warning");
+
+ info("Setting checkCompatibility to false");
+ AddonManager.checkCompatibility = false;
+
+ open_manager("addons://list/extension", function(aWindow) {
+ var hbox = aWindow.document.querySelector("#list-view hbox.global-warning-checkcompatibility");
+ is_element_visible(hbox, "Check Compatibility warning hbox should be visible");
+ var button = aWindow.document.querySelector("#list-view button.global-warning-checkcompatibility");
+ is_element_visible(button, "Check Compatibility warning button should be visible");
+
+ info("Clicking 'Enable' button");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+ is(AddonManager.checkCompatibility, true, "Check Compatibility pref should be cleared");
+ is_element_hidden(hbox, "Check Compatibility warning hbox should be hidden");
+ is_element_hidden(button, "Check Compatibility warning button should be hidden");
+
+ close_manager(aWindow, function() {
+ run_next_test();
+ });
+ });
+});
+
+add_test(function() {
+ info("Testing update security checking warning");
+
+ var pref = "extensions.checkUpdateSecurity";
+ info("Setting " + pref + " pref to false")
+ Services.prefs.setBoolPref(pref, false);
+
+ open_manager(null, function(aWindow) {
+ var hbox = aWindow.document.querySelector("#list-view hbox.global-warning-updatesecurity");
+ is_element_visible(hbox, "Check Update Security warning hbox should be visible");
+ var button = aWindow.document.querySelector("#list-view button.global-warning-updatesecurity");
+ is_element_visible(button, "Check Update Security warning button should be visible");
+
+ info("Clicking 'Enable' button");
+ EventUtils.synthesizeMouse(button, 2, 2, { }, aWindow);
+ is(Services.prefs.prefHasUserValue(pref), false, "Check Update Security pref should be cleared");
+ is_element_hidden(hbox, "Check Update Security warning hbox should be hidden");
+ is_element_hidden(button, "Check Update Security warning button should be hidden");
+
+ close_manager(aWindow, function() {
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js
new file mode 100644
index 000000000..52079a263
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_gmpProvider.js
@@ -0,0 +1,418 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+var {AddonTestUtils} = Cu.import("resource://testing-common/AddonManagerTesting.jsm", {});
+var GMPScope = Cu.import("resource://gre/modules/addons/GMPProvider.jsm");
+
+const TEST_DATE = new Date(2013, 0, 1, 12);
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gIsEnUsLocale;
+
+var gMockAddons = [];
+
+for (let plugin of GMPScope.GMP_PLUGINS) {
+ let mockAddon = Object.freeze({
+ id: plugin.id,
+ isValid: true,
+ isInstalled: false,
+ isEME: (plugin.id == "gmp-widevinecdm" ||
+ plugin.id.indexOf("gmp-eme-") == 0) ? true : false,
+ });
+ gMockAddons.push(mockAddon);
+}
+
+var gInstalledAddonId = "";
+var gInstallDeferred = null;
+var gPrefs = Services.prefs;
+var getKey = GMPScope.GMPPrefs.getPrefKey;
+
+function MockGMPInstallManager() {
+}
+
+MockGMPInstallManager.prototype = {
+ checkForAddons: () => Promise.resolve({
+ usedFallback: true,
+ gmpAddons: gMockAddons
+ }),
+
+ installAddon: addon => {
+ gInstalledAddonId = addon.id;
+ gInstallDeferred.resolve();
+ return Promise.resolve();
+ },
+};
+
+var gOptionsObserver = {
+ lastDisplayed: null,
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == AddonManager.OPTIONS_NOTIFICATION_DISPLAYED) {
+ this.lastDisplayed = aData;
+ }
+ }
+};
+
+function getInstallItem() {
+ let doc = gManagerWindow.document;
+ let list = doc.getElementById("addon-list");
+
+ let node = list.firstChild;
+ while (node) {
+ if (node.getAttribute("status") == "installing") {
+ return node;
+ }
+ node = node.nextSibling;
+ }
+
+ return null;
+}
+
+function openDetailsView(aId) {
+ let view = get_current_view(gManagerWindow);
+ Assert.equal(view.id, "list-view", "Should be in the list view to use this function");
+
+ let item = get_addon_element(gManagerWindow, aId);
+ Assert.ok(item, "Should have got add-on element.");
+ is_element_visible(item, "Add-on element should be visible.");
+
+ item.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+
+ let deferred = Promise.defer();
+ wait_for_view_load(gManagerWindow, deferred.resolve);
+ return deferred.promise;
+}
+
+add_task(function* initializeState() {
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP, true);
+ gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL, 0);
+
+ gManagerWindow = yield open_manager();
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ registerCleanupFunction(Task.async(function*() {
+ Services.obs.removeObserver(gOptionsObserver, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
+
+ for (let addon of gMockAddons) {
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id));
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id));
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id));
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id));
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id));
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id));
+ }
+ gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP);
+ gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL);
+ gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK);
+ gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_EME_ENABLED);
+ yield GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+ }));
+
+ let chrome = Cc["@mozilla.org/chrome/chrome-registry;1"].getService(Ci.nsIXULChromeRegistry);
+ gIsEnUsLocale = chrome.getSelectedLocale("global") == "en-US";
+
+ Services.obs.addObserver(gOptionsObserver, AddonManager.OPTIONS_NOTIFICATION_DISPLAYED, false);
+
+ // Start out with plugins not being installed, disabled and automatic updates
+ // disabled.
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true);
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false);
+ gPrefs.setIntPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id), 0);
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false);
+ gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), "");
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true);
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id), true);
+ }
+ yield GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+});
+
+add_task(function* testNotInstalledDisabled() {
+ Assert.ok(gCategoryUtilities.isTypeVisible("plugin"), "Plugin tab visible.");
+ yield gCategoryUtilities.openType("plugin");
+
+ for (let addon of gMockAddons) {
+ let item = get_addon_element(gManagerWindow, addon.id);
+ Assert.ok(item, "Got add-on element:" + addon.id);
+ item.parentNode.ensureElementIsVisible(item);
+ is(item.getAttribute("active"), "false");
+
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning");
+ is_element_hidden(el, "Warning notification is hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
+ is_element_visible(el, "disabled-postfix is visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
+ is_element_hidden(el, "Disable button not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
+ is_element_hidden(el, "Enable button not visible.");
+
+ let menu = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "state-menulist");
+ is_element_visible(menu, "State menu should be visible.");
+
+ let neverActivate = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "never-activate-menuitem");
+ is(menu.selectedItem, neverActivate, "Plugin state should be never-activate.");
+ }
+});
+
+add_task(function* testNotInstalledDisabledDetails() {
+ for (let addon of gMockAddons) {
+ yield openDetailsView(addon.id);
+ let doc = gManagerWindow.document;
+
+ let el = doc.getElementsByClassName("disabled-postfix")[0];
+ is_element_visible(el, "disabled-postfix is visible.");
+ el = doc.getElementById("detail-findUpdates-btn");
+ is_element_visible(el, "Find updates link is visible.");
+ el = doc.getElementById("detail-warning");
+ is_element_hidden(el, "Warning notification is hidden.");
+ el = doc.getElementsByTagName("setting")[0];
+
+ yield gCategoryUtilities.openType("plugin");
+ }
+});
+
+add_task(function* testNotInstalled() {
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true);
+ let item = get_addon_element(gManagerWindow, addon.id);
+ Assert.ok(item, "Got add-on element:" + addon.id);
+ item.parentNode.ensureElementIsVisible(item);
+ is(item.getAttribute("active"), "true");
+
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning");
+ is_element_visible(el, "Warning notification is visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
+ is_element_hidden(el, "disabled-postfix is hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
+ is_element_hidden(el, "Disable button not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
+ is_element_hidden(el, "Enable button not visible.");
+
+ let menu = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "state-menulist");
+ is_element_visible(menu, "State menu should be visible.");
+
+ let alwaysActivate = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "always-activate-menuitem");
+ is(menu.selectedItem, alwaysActivate, "Plugin state should be always-activate.");
+ }
+});
+
+add_task(function* testNotInstalledDetails() {
+ for (let addon of gMockAddons) {
+ yield openDetailsView(addon.id);
+ let doc = gManagerWindow.document;
+
+ let el = doc.getElementsByClassName("disabled-postfix")[0];
+ is_element_hidden(el, "disabled-postfix is hidden.");
+ el = doc.getElementById("detail-findUpdates-btn");
+ is_element_visible(el, "Find updates link is visible.");
+ el = doc.getElementById("detail-warning");
+ is_element_visible(el, "Warning notification is visible.");
+ el = doc.getElementsByTagName("setting")[0];
+
+ yield gCategoryUtilities.openType("plugin");
+ }
+});
+
+add_task(function* testInstalled() {
+ for (let addon of gMockAddons) {
+ gPrefs.setIntPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, addon.id),
+ TEST_DATE.getTime());
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id), false);
+ gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), "1.2.3.4");
+
+ let item = get_addon_element(gManagerWindow, addon.id);
+ Assert.ok(item, "Got add-on element.");
+ item.parentNode.ensureElementIsVisible(item);
+ is(item.getAttribute("active"), "true");
+
+ let el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "warning");
+ is_element_hidden(el, "Warning notification is hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "class", "disabled-postfix");
+ is_element_hidden(el, "disabled-postfix is hidden.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "disable-btn");
+ is_element_hidden(el, "Disable button not visible.");
+ el = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "enable-btn");
+ is_element_hidden(el, "Enable button not visible.");
+
+ let menu = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "state-menulist");
+ is_element_visible(menu, "State menu should be visible.");
+
+ let alwaysActivate = item.ownerDocument.getAnonymousElementByAttribute(item, "anonid", "always-activate-menuitem");
+ is(menu.selectedItem, alwaysActivate, "Plugin state should be always-activate.");
+ }
+});
+
+add_task(function* testInstalledDetails() {
+ for (let addon of gMockAddons) {
+ yield openDetailsView(addon.id);
+ let doc = gManagerWindow.document;
+
+ let el = doc.getElementsByClassName("disabled-postfix")[0];
+ is_element_hidden(el, "disabled-postfix is hidden.");
+ el = doc.getElementById("detail-findUpdates-btn");
+ is_element_visible(el, "Find updates link is visible.");
+ el = doc.getElementById("detail-warning");
+ is_element_hidden(el, "Warning notification is hidden.");
+ el = doc.getElementsByTagName("setting")[0];
+
+ let contextMenu = doc.getElementById("addonitem-popup");
+ let deferred = Promise.defer();
+ let listener = () => {
+ contextMenu.removeEventListener("popupshown", listener, false);
+ deferred.resolve();
+ };
+ contextMenu.addEventListener("popupshown", listener, false);
+ el = doc.getElementsByClassName("detail-view-container")[0];
+ EventUtils.synthesizeMouse(el, 4, 4, { }, gManagerWindow);
+ EventUtils.synthesizeMouse(el, 4, 4, { type: "contextmenu", button: 2 }, gManagerWindow);
+ yield deferred.promise;
+ let menuSep = doc.getElementById("addonitem-menuseparator");
+ is_element_hidden(menuSep, "Menu separator is hidden.");
+ contextMenu.hidePopup();
+
+ yield gCategoryUtilities.openType("plugin");
+ }
+});
+
+add_task(function* testInstalledGlobalEmeDisabled() {
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, false);
+ for (let addon of gMockAddons) {
+ let item = get_addon_element(gManagerWindow, addon.id);
+ if (addon.isEME) {
+ Assert.ok(!item, "Couldn't get add-on element.");
+ } else {
+ Assert.ok(item, "Got add-on element.");
+ }
+ }
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true);
+});
+
+add_task(function* testPreferencesButton() {
+
+ let prefValues = [
+ { enabled: false, version: "" },
+ { enabled: false, version: "1.2.3.4" },
+ { enabled: true, version: "" },
+ { enabled: true, version: "1.2.3.4" },
+ ];
+
+ for (let preferences of prefValues) {
+ dump("Testing preferences button with pref settings: " +
+ JSON.stringify(preferences) + "\n");
+ for (let addon of gMockAddons) {
+ yield close_manager(gManagerWindow);
+ gManagerWindow = yield open_manager();
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ gPrefs.setCharPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ preferences.version);
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id),
+ preferences.enabled);
+
+ yield gCategoryUtilities.openType("plugin");
+ let doc = gManagerWindow.document;
+ let item = get_addon_element(gManagerWindow, addon.id);
+
+ let button = doc.getAnonymousElementByAttribute(item, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ let deferred = Promise.defer();
+ wait_for_view_load(gManagerWindow, deferred.resolve);
+ yield deferred.promise;
+
+ is(gOptionsObserver.lastDisplayed, addon.id);
+ }
+ }
+});
+
+add_task(function* testUpdateButton() {
+ gPrefs.clearUserPref(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK);
+
+ let originalInstallManager = GMPScope.GMPInstallManager;
+ Object.defineProperty(GMPScope, "GMPInstallManager", {
+ value: MockGMPInstallManager,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+
+ for (let addon of gMockAddons) {
+ yield gCategoryUtilities.openType("plugin");
+ let doc = gManagerWindow.document;
+ let item = get_addon_element(gManagerWindow, addon.id);
+
+ gInstalledAddonId = "";
+ gInstallDeferred = Promise.defer();
+
+ let button = doc.getAnonymousElementByAttribute(item, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ let deferred = Promise.defer();
+ wait_for_view_load(gManagerWindow, deferred.resolve);
+ yield deferred.promise;
+
+ button = doc.getElementById("detail-findUpdates-btn");
+ Assert.ok(button != null, "Got detail-findUpdates-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ yield gInstallDeferred.promise;
+
+ Assert.equal(gInstalledAddonId, addon.id);
+ }
+ Object.defineProperty(GMPScope, "GMPInstallManager", {
+ value: originalInstallManager,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+});
+
+add_task(function* testEmeSupport() {
+ for (let addon of gMockAddons) {
+ gPrefs.clearUserPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id));
+ }
+ yield GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+
+ for (let addon of gMockAddons) {
+ yield gCategoryUtilities.openType("plugin");
+ let doc = gManagerWindow.document;
+ let item = get_addon_element(gManagerWindow, addon.id);
+ if (addon.id == GMPScope.EME_ADOBE_ID) {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6")) {
+ Assert.ok(item, "Adobe EME supported, found add-on element.");
+ } else {
+ Assert.ok(!item,
+ "Adobe EME not supported, couldn't find add-on element.");
+ }
+ } else if (addon.id == GMPScope.WIDEVINE_ID) {
+ if (AppConstants.isPlatformAndVersionAtLeast("win", "6") ||
+ AppConstants.platform == "macosx" ||
+ AppConstants.platform == "linux") {
+ Assert.ok(item, "Widevine supported, found add-on element.");
+ } else {
+ Assert.ok(!item,
+ "Widevine not supported, couldn't find add-on element.");
+ }
+ } else {
+ Assert.ok(item, "Found add-on element.");
+ }
+ }
+
+ for (let addon of gMockAddons) {
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id), true);
+ gPrefs.setBoolPref(getKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id), true);
+ }
+ yield GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+
+});
+
+add_task(function* test_cleanup() {
+ yield close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_hotfix.js b/toolkit/mozapps/extensions/test/browser/browser_hotfix.js
new file mode 100644
index 000000000..b7bb3f580
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_hotfix.js
@@ -0,0 +1,171 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+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_HOTFIX_CERTS = "extensions.hotfix.certs.";
+const PREF_EM_CERT_CHECKATTRIBUTES = "extensions.hotfix.cert.checkAttributes";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_URL = "app.update.url";
+
+const HOTFIX_ID = "hotfix@tests.mozilla.org";
+
+/*
+ * Register an addon install listener and return a promise that:
+ * resolves with the AddonInstall object if the install succeeds
+ * rejects with the AddonInstall if the install fails
+ */
+function promiseInstallListener() {
+ return new Promise((resolve, reject) => {
+ let listener = {
+ onInstallEnded: ai => {
+ AddonManager.removeInstallListener(listener);
+ resolve(ai);
+ },
+ onDownloadCancelled: ai => {
+ AddonManager.removeInstallListener(listener);
+ reject(ai);
+ }
+ };
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+function promiseSuccessfulInstall() {
+ return promiseInstallListener().then(
+ aInstall => {
+ ok(true, "Should have seen the install complete");
+ is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
+ aInstall.addon.uninstall();
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+ },
+ aInstall => {
+ ok(false, "Should not have seen the download cancelled");
+ is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
+ });
+}
+
+function promiseFailedInstall() {
+ return promiseInstallListener().then(
+ aInstall => {
+ ok(false, "Should not have seen the install complete");
+ is(aInstall.addon.id, HOTFIX_ID, "Should have installed the right add-on");
+ aInstall.addon.uninstall();
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+ },
+ aInstall => {
+ ok(true, "Should have seen the download cancelled");
+ is(aInstall.addon.id, HOTFIX_ID, "Should have seen the right add-on");
+ });
+}
+
+add_task(function setup() {
+ var oldAusUrl = Services.prefs.getDefaultBranch(null).getCharPref(PREF_APP_UPDATE_URL);
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_APP_UPDATE_URL, TESTROOT + "ausdummy.xml");
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+ Services.prefs.setBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, false);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_ID, HOTFIX_ID);
+ var oldURL = Services.prefs.getCharPref(PREF_EM_HOTFIX_URL);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "signed_hotfix.rdf");
+
+ registerCleanupFunction(function() {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+ Services.prefs.getDefaultBranch(null).setCharPref(PREF_APP_UPDATE_URL, oldAusUrl);
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_ID);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, oldURL);
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+ Services.prefs.clearUserPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+
+ Services.prefs.clearUserPref(PREF_EM_CERT_CHECKATTRIBUTES);
+ var prefs = Services.prefs.getChildList(PREF_EM_HOTFIX_CERTS);
+ prefs.forEach(Services.prefs.clearUserPref);
+ });
+});
+
+add_task(function* check_no_cert_checks() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
+ yield Promise.all([
+ promiseSuccessfulInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+});
+
+add_task(function* check_wrong_cert_fingerprint() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
+
+ yield Promise.all([
+ promiseFailedInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+});
+
+add_task(function* check_right_cert_fingerprint() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+ yield Promise.all([
+ promiseSuccessfulInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+});
+
+add_task(function* check_multi_cert_fingerprint_1() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "foo");
+
+ yield Promise.all([
+ promiseSuccessfulInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
+});
+
+add_task(function* check_multi_cert_fingerprint_2() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "foo");
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+ yield Promise.all([
+ promiseSuccessfulInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "2.sha1Fingerprint");
+});
+
+add_task(function* check_no_cert_no_checks() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, false);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, TESTROOT + "unsigned_hotfix.rdf");
+
+ yield Promise.all([
+ promiseSuccessfulInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+});
+
+add_task(function* check_no_cert_cert_fingerprint_check() {
+ Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", "3E:B9:4E:07:12:FE:3C:01:41:46:13:46:FC:84:52:1A:8C:BE:1D:A2");
+
+ yield Promise.all([
+ promiseFailedInstall(),
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js
new file mode 100644
index 000000000..e2814ddf4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings.js
@@ -0,0 +1,680 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests various aspects of the details view
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+const SETTINGS_ROWS = 9;
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+var observer = {
+ lastDisplayed: null,
+ callback: null,
+ checkDisplayed: function(aExpected) {
+ is(this.lastDisplayed, aExpected, "'addon-options-displayed' notification should have fired");
+ this.lastDisplayed = null;
+ },
+ checkNotDisplayed: function() {
+ is(this.lastDisplayed, null, "'addon-options-displayed' notification should not have fired");
+ },
+ lastHidden: null,
+ checkHidden: function(aExpected) {
+ is(this.lastHidden, aExpected, "'addon-options-hidden' notification should have fired");
+ this.lastHidden = null;
+ },
+ checkNotHidden: function() {
+ is(this.lastHidden, null, "'addon-options-hidden' notification should not have fired");
+ },
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == AddonManager.OPTIONS_NOTIFICATION_DISPLAYED) {
+ this.lastDisplayed = aData;
+ // Test if the binding has applied before the observers are notified. We test the second setting here,
+ // because the code operates on the first setting and we want to check it applies to all.
+ var setting = aSubject.querySelector("rows > setting[first-row] ~ setting");
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(setting, "class", "preferences-title");
+ isnot(input, null, "XBL binding should be applied");
+
+ // Add some extra height to the scrolling pane to ensure that it needs to scroll when appropriate.
+ gManagerWindow.document.getElementById("detail-controls").style.marginBottom = "1000px";
+
+ if (this.callback) {
+ var tempCallback = this.callback;
+ this.callback = null;
+ tempCallback();
+ }
+ } else if (aTopic == AddonManager.OPTIONS_NOTIFICATION_HIDDEN) {
+ this.lastHidden = aData;
+ }
+ }
+};
+
+function installAddon(aCallback) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_inlinesettings1.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function checkScrolling(aShouldHaveScrolled) {
+ var detailView = gManagerWindow.document.getElementById("detail-view");
+ var boxObject = detailView.boxObject;
+ ok(detailView.scrollHeight > boxObject.height, "Page should require scrolling");
+ if (aShouldHaveScrolled)
+ isnot(detailView.scrollTop, 0, "Page should have scrolled");
+ else
+ is(detailView.scrollTop, 0, "Page should not have scrolled");
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "inlinesettings2@tests.mozilla.org",
+ name: "Inline Settings (Regular)",
+ version: "1",
+ optionsURL: CHROMEROOT + "options.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_DISABLE,
+ }, {
+ id: "inlinesettings3@tests.mozilla.org",
+ name: "Inline Settings (More Options)",
+ description: "Tests for option types introduced after Mozilla 7.0",
+ version: "1",
+ optionsURL: CHROMEROOT + "more_options.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE
+ }, {
+ id: "noninlinesettings@tests.mozilla.org",
+ name: "Non-Inline Settings",
+ version: "1",
+ optionsURL: CHROMEROOT + "addon_prefs.xul"
+ }]);
+
+ installAddon(function () {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ Services.obs.addObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ false);
+ Services.obs.addObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN,
+ false);
+
+ run_next_test();
+ });
+ });
+}
+
+function end_test() {
+ Services.obs.removeObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
+
+ Services.prefs.clearUserPref("extensions.inlinesettings1.bool");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.boolint");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.integer");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.string");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.color");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.file");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.directory");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioBool");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioInt");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioString");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.menulist");
+
+ MockFilePicker.cleanup();
+
+ close_manager(gManagerWindow, function() {
+ observer.checkHidden("inlinesettings3@tests.mozilla.org");
+ Services.obs.removeObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN);
+
+ AddonManager.getAddonByID("inlinesettings1@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+ finish();
+ });
+ });
+}
+
+// Addon with options.xul
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE, "Options should be inline type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ run_next_test();
+});
+
+// Addon with inline preferences as optionsURL
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE, "Options should be inline type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ run_next_test();
+});
+
+// Addon with non-inline preferences as optionsURL
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "noninlinesettings@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_DIALOG, "Options should be dialog type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ run_next_test();
+});
+
+// Addon with options.xul, also a test for the setting.xml bindings
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+ is(gManagerWindow.gViewController.currentViewId,
+ "addons://detail/inlinesettings1%40tests.mozilla.org/preferences",
+ "Current view should scroll to preferences");
+ checkScrolling(true);
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
+ Services.prefs.setBoolPref("extensions.inlinesettings1.bool", false);
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "input");
+ isnot(input.checked, true, "Checkbox should have initial value");
+ is(input.label, "Check box label", "Checkbox should be labelled");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ is(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getBoolPref("extensions.inlinesettings1.bool"), true, "Bool pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ isnot(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getBoolPref("extensions.inlinesettings1.bool"), false, "Bool pref should have been updated");
+
+ ok(!settings[1].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings1.boolint", 0);
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[1], "anonid", "input");
+ isnot(input.checked, true, "Checkbox should have initial value");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ is(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.boolint"), 1, "BoolInt pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ isnot(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.boolint"), 2, "BoolInt pref should have been updated");
+
+ ok(!settings[2].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings1.integer", 0);
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
+ is(input.value, "0", "Number box should have initial value");
+ input.select();
+ EventUtils.synthesizeKey("1", {}, gManagerWindow);
+ EventUtils.synthesizeKey("3", {}, gManagerWindow);
+ is(input.value, "13", "Number box should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.integer"), 13, "Integer pref should have been updated");
+ EventUtils.synthesizeKey("VK_DOWN", {}, gManagerWindow);
+ is(input.value, "12", "Number box should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.integer"), 12, "Integer pref should have been updated");
+
+ ok(!settings[3].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings1.string", "foo");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[3], "anonid", "input");
+ is(input.value, "foo", "Text box should have initial value");
+ input.select();
+ EventUtils.synthesizeKey("b", {}, gManagerWindow);
+ EventUtils.synthesizeKey("a", {}, gManagerWindow);
+ EventUtils.synthesizeKey("r", {}, gManagerWindow);
+ is(input.value, "bar", "Text box should have updated value");
+ input.value += "\u03DE"; // Cheat to add this non-ASCII character without typing it.
+ EventUtils.synthesizeKey("/", {}, gManagerWindow);
+ is(input.value, "bar\u03DE/", "Text box should have updated value");
+ is(gManagerWindow.document.getBindingParent(gManagerWindow.document.activeElement), input, "Search box should not have focus");
+ is(Preferences.get("extensions.inlinesettings1.string", "wrong"), "bar\u03DE/", "String pref should have been updated");
+
+ ok(!settings[4].hasAttribute("first-row"), "Not the first row");
+ input = settings[4].firstElementChild;
+ is(input.value, "1", "Menulist should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("b", {}, gManagerWindow);
+ is(input.value, "2", "Menulist should have updated value");
+ is(gManagerWindow._testValue, "2", "Menulist oncommand handler should've updated the test value");
+ delete gManagerWindow._testValue;
+
+ ok(!settings[5].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings1.color", "#FF0000");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[5], "anonid", "input");
+ is(input.color, "#FF0000", "Color picker should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {}, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, gManagerWindow);
+ input.hidePopup();
+ is(input.color, "#FF9900", "Color picker should have updated value");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.color"), "#FF9900", "Color pref should have been updated");
+
+ try {
+ ok(!settings[6].hasAttribute("first-row"), "Not the first row");
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "button");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "input");
+ is(input.value, "", "Label value should be empty");
+ is(input.tooltipText, "", "Label tooltip should be empty");
+
+ var testFile = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ testFile.append("\u2622");
+ var curProcD = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+
+ MockFilePicker.returnFiles = [testFile];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
+ is(input.value, testFile.path, "Label value should match file chosen");
+ is(input.tooltipText, testFile.path, "Label tooltip should match file chosen");
+ is(Preferences.get("extensions.inlinesettings1.file", "wrong"), testFile.path, "File pref should match file chosen");
+
+ MockFilePicker.returnFiles = [curProcD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
+ is(input.value, testFile.path, "Label value should not have changed");
+ is(input.tooltipText, testFile.path, "Label tooltip should not have changed");
+ is(Preferences.get("extensions.inlinesettings1.file", "wrong"), testFile.path, "File pref should not have changed");
+
+ ok(!settings[7].hasAttribute("first-row"), "Not the first row");
+ button = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "button");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "input");
+ is(input.value, "", "Label value should be empty");
+ is(input.tooltipText, "", "Label tooltip should be empty");
+
+ MockFilePicker.returnFiles = [testFile];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
+ is(input.value, testFile.path, "Label value should match file chosen");
+ is(input.tooltipText, testFile.path, "Label tooltip should match file chosen");
+ is(Preferences.get("extensions.inlinesettings1.directory", "wrong"), testFile.path, "Directory pref should match file chosen");
+
+ MockFilePicker.returnFiles = [curProcD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
+ is(input.value, testFile.path, "Label value should not have changed");
+ is(input.tooltipText, testFile.path, "Label tooltip should not have changed");
+ is(Preferences.get("extensions.inlinesettings1.directory", "wrong"), testFile.path, "Directory pref should not have changed");
+
+ var unsizedInput = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
+ var sizedInput = gManagerWindow.document.getAnonymousElementByAttribute(settings[8], "anonid", "input");
+ is(unsizedInput.clientWidth > sizedInput.clientWidth, true, "Input with size attribute should be smaller than input without");
+ } finally {
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ }
+ });
+});
+
+// Tests for the setting.xml bindings introduced after Mozilla 7
+add_test(function() {
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings3@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings3@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 4, "Grid should have settings children");
+
+ ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
+ Services.prefs.setBoolPref("extensions.inlinesettings3.radioBool", false);
+ var radios = settings[0].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getBoolPref("extensions.inlinesettings3.radioBool"), true, "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[1], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getBoolPref("extensions.inlinesettings3.radioBool"), false, "Radio pref should have been updated");
+
+ ok(!settings[1].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings3.radioInt", 5);
+ radios = settings[1].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ isnot(radios[2].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.radioInt"), 4, "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.radioInt"), 6, "Radio pref should have been updated");
+
+ ok(!settings[2].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings3.radioString", "juliet");
+ radios = settings[2].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ isnot(radios[2].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "india", "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
+ is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "kilo \u338F", "Radio pref should have been updated");
+
+ ok(!settings[3].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 8);
+ var input = settings[3].firstElementChild;
+ is(input.value, "8", "Menulist should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("n", {}, gManagerWindow);
+ is(input.value, "9", "Menulist should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.menulist"), 9, "Menulist pref should have been updated");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with inline preferences as optionsURL
+add_test(function() {
+ observer.checkHidden("inlinesettings3@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings2@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 5, "Grid should have settings children");
+
+ var node = settings[0];
+ node = settings[0];
+ is_element_hidden(node, "Unsupported settings should not be visible");
+ ok(!node.hasAttribute("first-row"), "Hidden row is not the first row");
+
+ node = settings[1];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(node.hasAttribute("first-row"), "First visible row should have first-row attribute");
+ var description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "Description Attribute", "Description node should contain description");
+
+ node = settings[2];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(!node.hasAttribute("first-row"), "Not the first row");
+ description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "Description Text Node", "Description node should contain description");
+
+ node = settings[3];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(!node.hasAttribute("first-row"), "Not the first row");
+ description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "This is a test, all this text should be visible", "Description node should contain description");
+ var button = node.firstElementChild;
+ isnot(button, null, "There should be a button");
+
+ node = settings[4];
+ is_element_hidden(node, "Unsupported settings should not be visible");
+ ok(!node.hasAttribute("first-row"), "Hidden row is not the first row");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with non-inline preferences as optionsURL
+add_test(function() {
+ observer.checkHidden("inlinesettings2@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "noninlinesettings@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkNotDisplayed();
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ var button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with options.xul, disabling and enabling should hide and show settings UI
+add_test(function() {
+ observer.checkNotHidden();
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+ is(gManagerWindow.gViewController.currentViewId,
+ "addons://detail/inlinesettings1%40tests.mozilla.org",
+ "Current view should not scroll to preferences");
+ checkScrolling(false);
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ // disable
+ var button = gManagerWindow.document.getElementById("detail-disable-btn");
+ button.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ gCategoryUtilities.openType("extension", function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ // enable
+ var button = gManagerWindow.document.getElementById("detail-enable-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ observer.callback = function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+
+ settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ };
+ });
+ });
+ });
+});
+
+
+// Addon with options.xul that requires a restart to disable,
+// disabling and enabling should not hide and show settings UI.
+add_test(function() {
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings2@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ ok(settings.length > 0, "Grid should have settings children");
+
+ // disable
+ var button = gManagerWindow.document.getElementById("detail-disable-btn");
+ button.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ observer.checkNotHidden();
+
+ settings = grid.querySelectorAll("rows > setting");
+ ok(settings.length > 0, "Grid should still have settings children");
+
+ // cancel pending disable
+ button = gManagerWindow.document.getElementById("detail-enable-btn");
+ button.scrollIntoView();
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ observer.checkNotDisplayed();
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Tests bindings with existing prefs.
+add_test(function() {
+ observer.checkHidden("inlinesettings2@tests.mozilla.org");
+
+ // Ensure these prefs are set. They should be set above, but somebody might
+ // change the tests above.
+ var profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ Services.prefs.setBoolPref("extensions.inlinesettings1.bool", false);
+ Services.prefs.setIntPref("extensions.inlinesettings1.boolint", 1);
+ Services.prefs.setIntPref("extensions.inlinesettings1.integer", 12);
+ Preferences.set("extensions.inlinesettings1.string", "bar\u03DE/");
+ Services.prefs.setCharPref("extensions.inlinesettings1.color", "#FF9900");
+ Services.prefs.setCharPref("extensions.inlinesettings1.file", profD.path);
+ Services.prefs.setCharPref("extensions.inlinesettings1.directory", profD.path);
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "input");
+ is(input.checked, false, "Checkbox should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[1], "anonid", "input");
+ is(input.checked, true, "Checkbox should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
+ is(input.value, "12", "Number box should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[3], "anonid", "input");
+ is(input.value, "bar\u03DE/", "Text box should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[5], "anonid", "input");
+ is(input.color, "#FF9900", "Color picker should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "input");
+ is(input.value, profD.path, "Label should have initial value");
+ is(input.tooltipText, profD.path, "Label tooltip should have initial value");
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "input");
+ is(input.value, profD.path, "Label value should have initial value");
+ is(input.tooltipText, profD.path, "Label tooltip should have initial value");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Tests bindings with existing prefs.
+add_test(function() {
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ // Ensure these prefs are set. They should be set above, but somebody might
+ // change the tests above.
+ Services.prefs.setBoolPref("extensions.inlinesettings3.radioBool", false);
+ Services.prefs.setIntPref("extensions.inlinesettings3.radioInt", 6);
+ Preferences.set("extensions.inlinesettings3.radioString", "kilo \u338F");
+ Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 9);
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings3@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings3@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+
+ var radios = settings[0].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+
+ radios = settings[1].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ isnot(radios[1].selected, true, "Correct radio button should be selected");
+ is(radios[2].selected, true, "Correct radio button should be selected");
+
+ radios = settings[2].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ isnot(radios[1].selected, true, "Correct radio button should be selected");
+ is(radios[2].selected, true, "Correct radio button should be selected");
+
+ var input = settings[3].firstElementChild;
+ is(input.value, "9", "Menulist should have initial value");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
new file mode 100644
index 000000000..5a704530a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* globals TestUtils */
+
+var {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+var gAddon;
+var gOtherAddon;
+var gManagerWindow;
+var gCategoryUtilities;
+
+var installedAddons = [];
+
+function installAddon(details) {
+ let id = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)
+ .generateUUID().number;
+ if (!details.manifest) {
+ details.manifest = {};
+ }
+ details.manifest.applications = {gecko: {id}};
+ let xpi = Extension.generateXPI(details);
+
+ return AddonManager.installTemporaryAddon(xpi).then(addon => {
+ SimpleTest.registerCleanupFunction(function() {
+ addon.uninstall();
+
+ Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
+ xpi.remove(false);
+ });
+
+ return addon;
+ });
+}
+
+add_task(function*() {
+ gAddon = yield installAddon({
+ manifest: {
+ "options_ui": {
+ "page": "options.html",
+ }
+ },
+
+ files: {
+ "options.html": `<!DOCTYPE html>
+ <html>
+ <head>
+ <meta charset="UTF-8">
+ <style type="text/css">
+ body > p {
+ height: 300px;
+ margin: 0;
+ }
+ body.bigger > p {
+ height: 600px;
+ }
+ </style>
+ </head>
+ <body>
+ <p>The quick mauve fox jumps over the opalescent dog.</p>
+ </body>
+ </html>`,
+ },
+ });
+
+ // Create another add-on with no inline options, to verify that detail
+ // view switches work correctly.
+ gOtherAddon = yield installAddon({});
+
+ gManagerWindow = yield open_manager("addons://list/extension");
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
+
+
+function* openDetailsBrowser(addonId) {
+ var addon = get_addon_element(gManagerWindow, addonId);
+
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
+ "Options should be inline browser type");
+
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+
+ is_element_visible(button, "Preferences button should be visible");
+
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ yield TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ (subject, data) => data == addonId);
+
+ is(gManagerWindow.gViewController.currentViewId,
+ `addons://detail/${encodeURIComponent(addonId)}/preferences`,
+ "Current view should scroll to preferences");
+
+ var browser = gManagerWindow.document.querySelector(
+ "#detail-grid > rows > .inline-options-browser");
+ var rows = browser.parentNode;
+
+ ok(browser, "Grid should have a browser child");
+ is(browser.localName, "browser", "Grid should have a browser child");
+ is(browser.currentURI.spec, addon.mAddon.optionsURL, "Browser has the expected options URL loaded")
+
+ is(browser.clientWidth, rows.clientWidth,
+ "Browser should be the same width as its parent node");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ return browser;
+}
+
+
+add_task(function* test_inline_browser_addon() {
+ let browser = yield openDetailsBrowser(gAddon.id);
+
+ let body = browser.contentDocument.body;
+
+ function checkHeights(expected) {
+ is(body.clientHeight, expected, `Document body should be ${expected}px tall`);
+ is(body.clientHeight, body.scrollHeight,
+ "Document body should be tall enough to fit its contents");
+
+ let heightDiff = browser.clientHeight - expected;
+ ok(heightDiff >= 0 && heightDiff < 50,
+ "Browser should be slightly taller than the document body");
+ }
+
+ // Delay long enough to avoid hitting our resize rate limit.
+ let delay = () => new Promise(resolve => setTimeout(resolve, 300));
+
+ checkHeights(300);
+
+ info("Increase the document height, and expect the browser to grow correspondingly");
+ body.classList.toggle("bigger");
+
+ yield delay();
+
+ checkHeights(600);
+
+ info("Decrease the document height, and expect the browser to shrink correspondingly");
+ body.classList.toggle("bigger");
+
+ yield delay();
+
+ checkHeights(300);
+
+ yield new Promise(resolve =>
+ gCategoryUtilities.openType("extension", resolve));
+
+ browser = gManagerWindow.document.querySelector(
+ ".inline-options-browser");
+
+ is(browser, null, "Options browser should be removed from the document");
+});
+
+
+// Test that loading an add-on with no inline browser works as expected
+// after having viewed our main test add-on.
+add_task(function* test_plain_addon() {
+ var addon = get_addon_element(gManagerWindow, gOtherAddon.id);
+
+ is(addon.mAddon.optionsType, null, "Add-on should have no options");
+
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ yield EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 1 }, gManagerWindow);
+
+ EventUtils.synthesizeMouseAtCenter(addon, { clickCount: 2 }, gManagerWindow);
+
+ yield BrowserTestUtils.waitForEvent(gManagerWindow, "ViewChanged");
+
+ is(gManagerWindow.gViewController.currentViewId,
+ `addons://detail/${encodeURIComponent(gOtherAddon.id)}`,
+ "Detail view should be open");
+
+ var browser = gManagerWindow.document.querySelector(
+ "#detail-grid > rows > .inline-options-browser");
+
+ is(browser, null, "Detail view should have no inline browser");
+
+ yield new Promise(resolve =>
+ gCategoryUtilities.openType("extension", resolve));
+});
+
+
+// Test that loading the original add-on details successfully creates a
+// browser.
+add_task(function* test_inline_browser_addon_again() {
+ let browser = yield openDetailsBrowser(gAddon.id);
+
+ yield new Promise(resolve =>
+ gCategoryUtilities.openType("extension", resolve));
+
+ browser = gManagerWindow.document.querySelector(
+ ".inline-options-browser");
+
+ is(browser, null, "Options browser should be removed from the document");
+});
+
+add_task(function*() {
+ yield close_manager(gManagerWindow);
+
+ gManagerWindow = null;
+ gCategoryUtilities = null;
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_custom.js b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_custom.js
new file mode 100644
index 000000000..ecd10852d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_custom.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests various aspects of the details view
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+function installAddon(aCallback) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_inlinesettings1_custom.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ installAddon(function () {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ AddonManager.getAddonByID("inlinesettings1@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+ finish();
+ });
+ });
+}
+
+// Addon with options.xul, with custom <setting> binding
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE, "Options should be inline type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ run_next_test();
+});
+
+// Addon with options.xul, also a test for the setting.xml bindings
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gManagerWindow.gViewController.currentViewId,
+ "addons://detail/inlinesettings1%40tests.mozilla.org/preferences",
+ "Current view should scroll to preferences");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 1, "Grid should have settings children");
+
+ ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
+
+ var style = window.getComputedStyle(settings[0], null);
+ is(style.getPropertyValue("background-color"), "rgb(0, 0, 255)", "Background color should be set");
+ is(style.getPropertyValue("display"), "-moz-grid-line", "Display should be set");
+ is(style.getPropertyValue("-moz-binding"), 'url("chrome://inlinesettings/content/binding.xml#custom")', "Binding should be set");
+
+ var label = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "label");
+ is(label.textContent, "Custom", "Localized string should be shown");
+
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "input");
+ isnot(input, null, "Binding should be applied");
+ is(input.value, "Woah!", "Binding should be applied");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js
new file mode 100644
index 000000000..ce618b7fa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_info.js
@@ -0,0 +1,574 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests various aspects of the details view
+Components.utils.import("resource://gre/modules/Preferences.jsm");
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+const SETTINGS_ROWS = 8;
+
+var MockFilePicker = SpecialPowers.MockFilePicker;
+MockFilePicker.init(window);
+
+var observer = {
+ lastDisplayed: null,
+ callback: null,
+ checkDisplayed: function(aExpected) {
+ is(this.lastDisplayed, aExpected, "'addon-options-displayed' notification should have fired");
+ this.lastDisplayed = null;
+ },
+ checkNotDisplayed: function() {
+ is(this.lastDisplayed, null, "'addon-options-displayed' notification should not have fired");
+ },
+ lastHidden: null,
+ checkHidden: function(aExpected) {
+ is(this.lastHidden, aExpected, "'addon-options-hidden' notification should have fired");
+ this.lastHidden = null;
+ },
+ checkNotHidden: function() {
+ is(this.lastHidden, null, "'addon-options-hidden' notification should not have fired");
+ },
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == AddonManager.OPTIONS_NOTIFICATION_DISPLAYED) {
+ this.lastDisplayed = aData;
+ // Test if the binding has applied before the observers are notified. We test the second setting here,
+ // because the code operates on the first setting and we want to check it applies to all.
+ var setting = aSubject.querySelector("rows > setting[first-row] ~ setting");
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(setting, "class", "preferences-title");
+ isnot(input, null, "XBL binding should be applied");
+
+ // Add some extra height to the scrolling pane to ensure that it needs to scroll when appropriate.
+ gManagerWindow.document.getElementById("detail-controls").style.marginBottom = "1000px";
+
+ if (this.callback) {
+ var tempCallback = this.callback;
+ this.callback = null;
+ tempCallback();
+ }
+ } else if (aTopic == AddonManager.OPTIONS_NOTIFICATION_HIDDEN) {
+ this.lastHidden = aData;
+ }
+ }
+};
+
+function installAddon(aCallback) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_inlinesettings1_info.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function checkScrolling(aShouldHaveScrolled) {
+ var detailView = gManagerWindow.document.getElementById("detail-view");
+ var boxObject = detailView.boxObject;
+ ok(detailView.scrollHeight > boxObject.height, "Page should require scrolling");
+ if (aShouldHaveScrolled)
+ isnot(detailView.scrollTop, 0, "Page should have scrolled");
+ else
+ is(detailView.scrollTop, 0, "Page should not have scrolled");
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "inlinesettings2@tests.mozilla.org",
+ name: "Inline Settings (Regular)",
+ version: "1",
+ optionsURL: CHROMEROOT + "options.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE_INFO,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_DISABLE,
+ }, {
+ id: "inlinesettings3@tests.mozilla.org",
+ name: "Inline Settings (More Options)",
+ description: "Tests for option types introduced after Mozilla 7.0",
+ version: "1",
+ optionsURL: CHROMEROOT + "more_options.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_INLINE_INFO
+ }, {
+ id: "noninlinesettings@tests.mozilla.org",
+ name: "Non-Inline Settings",
+ version: "1",
+ optionsURL: CHROMEROOT + "addon_prefs.xul"
+ }]);
+
+ installAddon(function () {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ Services.obs.addObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ false);
+ Services.obs.addObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN,
+ false);
+
+ run_next_test();
+ });
+ });
+}
+
+function end_test() {
+ Services.obs.removeObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_DISPLAYED);
+
+ Services.prefs.clearUserPref("extensions.inlinesettings1.bool");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.boolint");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.integer");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.string");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.color");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.file");
+ Services.prefs.clearUserPref("extensions.inlinesettings1.directory");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioBool");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioInt");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.radioString");
+ Services.prefs.clearUserPref("extensions.inlinesettings3.menulist");
+
+ MockFilePicker.cleanup();
+
+ close_manager(gManagerWindow, function() {
+ observer.checkHidden("inlinesettings2@tests.mozilla.org");
+ Services.obs.removeObserver(observer,
+ AddonManager.OPTIONS_NOTIFICATION_HIDDEN);
+
+ AddonManager.getAddonByID("inlinesettings1@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+ finish();
+ });
+ });
+}
+
+// Addon with options.xul
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_INFO, "Options should be inline info type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_hidden(button, "Preferences button should be hidden");
+
+ run_next_test();
+});
+
+// Addon with inline preferences as optionsURL
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_INFO, "Options should be inline info type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_hidden(button, "Preferences button should be hidden");
+
+ run_next_test();
+});
+
+// Addon with non-inline preferences as optionsURL
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "noninlinesettings@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_DIALOG, "Options should be dialog type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ run_next_test();
+});
+
+// Addon with options.xul, also a test for the setting.xml bindings
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
+ Services.prefs.setBoolPref("extensions.inlinesettings1.bool", false);
+ var input = gManagerWindow.document.getAnonymousElementByAttribute(settings[0], "anonid", "input");
+ isnot(input.checked, true, "Checkbox should have initial value");
+ is(input.label, "Check box label", "Checkbox should be labelled");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ is(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getBoolPref("extensions.inlinesettings1.bool"), true, "Bool pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ isnot(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getBoolPref("extensions.inlinesettings1.bool"), false, "Bool pref should have been updated");
+
+ ok(!settings[1].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings1.boolint", 0);
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[1], "anonid", "input");
+ isnot(input.checked, true, "Checkbox should have initial value");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ is(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.boolint"), 1, "BoolInt pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(input, { clickCount: 1 }, gManagerWindow);
+ isnot(input.checked, true, "Checkbox should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.boolint"), 2, "BoolInt pref should have been updated");
+
+ ok(!settings[2].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings1.integer", 0);
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[2], "anonid", "input");
+ is(input.value, "0", "Number box should have initial value");
+ input.select();
+ EventUtils.synthesizeKey("1", {}, gManagerWindow);
+ EventUtils.synthesizeKey("3", {}, gManagerWindow);
+ is(input.value, "13", "Number box should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.integer"), 13, "Integer pref should have been updated");
+ EventUtils.synthesizeKey("VK_DOWN", {}, gManagerWindow);
+ is(input.value, "12", "Number box should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings1.integer"), 12, "Integer pref should have been updated");
+
+ ok(!settings[3].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings1.string", "foo");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[3], "anonid", "input");
+ is(input.value, "foo", "Text box should have initial value");
+ input.select();
+ EventUtils.synthesizeKey("b", {}, gManagerWindow);
+ EventUtils.synthesizeKey("a", {}, gManagerWindow);
+ EventUtils.synthesizeKey("r", {}, gManagerWindow);
+ is(input.value, "bar", "Text box should have updated value");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.string"), "bar", "String pref should have been updated");
+
+ ok(!settings[4].hasAttribute("first-row"), "Not the first row");
+ input = settings[4].firstElementChild;
+ is(input.value, "1", "Menulist should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("b", {}, gManagerWindow);
+ is(input.value, "2", "Menulist should have updated value");
+ is(gManagerWindow._testValue, "2", "Menulist oncommand handler should've updated the test value");
+ delete gManagerWindow._testValue;
+
+ ok(!settings[5].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings1.color", "#FF0000");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[5], "anonid", "input");
+ is(input.color, "#FF0000", "Color picker should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("VK_RIGHT", {}, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RIGHT", {}, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", {}, gManagerWindow);
+ input.hidePopup();
+ is(input.color, "#FF9900", "Color picker should have updated value");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.color"), "#FF9900", "Color pref should have been updated");
+
+ try {
+ ok(!settings[6].hasAttribute("first-row"), "Not the first row");
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "button");
+
+ // Workaround for bug 1155324 - we need to ensure that the button is scrolled into view.
+ button.scrollIntoView();
+
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[6], "anonid", "input");
+ is(input.value, "", "Label value should be empty");
+ is(input.tooltipText, "", "Label tooltip should be empty");
+
+ var profD = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ var curProcD = Services.dirsvc.get("CurProcD", Ci.nsIFile);
+
+ MockFilePicker.returnFiles = [profD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
+ is(input.value, profD.path, "Label value should match file chosen");
+ is(input.tooltipText, profD.path, "Label tooltip should match file chosen");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.file"), profD.path, "File pref should match file chosen");
+
+ MockFilePicker.returnFiles = [curProcD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeOpen, "File picker mode should be open file");
+ is(input.value, profD.path, "Label value should not have changed");
+ is(input.tooltipText, profD.path, "Label tooltip should not have changed");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.file"), profD.path, "File pref should not have changed");
+
+ ok(!settings[7].hasAttribute("first-row"), "Not the first row");
+ button = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "button");
+ input = gManagerWindow.document.getAnonymousElementByAttribute(settings[7], "anonid", "input");
+ is(input.value, "", "Label value should be empty");
+ is(input.tooltipText, "", "Label tooltip should be empty");
+
+ MockFilePicker.returnFiles = [profD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnOK;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
+ is(input.value, profD.path, "Label value should match file chosen");
+ is(input.tooltipText, profD.path, "Label tooltip should match file chosen");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.directory"), profD.path, "Directory pref should match file chosen");
+
+ MockFilePicker.returnFiles = [curProcD];
+ MockFilePicker.returnValue = Ci.nsIFilePicker.returnCancel;
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ is(MockFilePicker.mode, Ci.nsIFilePicker.modeGetFolder, "File picker mode should be directory");
+ is(input.value, profD.path, "Label value should not have changed");
+ is(input.tooltipText, profD.path, "Label tooltip should not have changed");
+ is(Services.prefs.getCharPref("extensions.inlinesettings1.directory"), profD.path, "Directory pref should not have changed");
+
+ } finally {
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ }
+ });
+});
+
+// Tests for the setting.xml bindings introduced after Mozilla 7
+add_test(function() {
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings3@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings3@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 4, "Grid should have settings children");
+
+ ok(settings[0].hasAttribute("first-row"), "First visible row should have first-row attribute");
+ Services.prefs.setBoolPref("extensions.inlinesettings3.radioBool", false);
+ var radios = settings[0].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getBoolPref("extensions.inlinesettings3.radioBool"), true, "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[1], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getBoolPref("extensions.inlinesettings3.radioBool"), false, "Radio pref should have been updated");
+
+ ok(!settings[1].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings3.radioInt", 5);
+ radios = settings[1].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ isnot(radios[2].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.radioInt"), 4, "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.radioInt"), 6, "Radio pref should have been updated");
+
+ ok(!settings[2].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setCharPref("extensions.inlinesettings3.radioString", "juliet");
+ radios = settings[2].getElementsByTagName("radio");
+ isnot(radios[0].selected, true, "Correct radio button should be selected");
+ is(radios[1].selected, true, "Correct radio button should be selected");
+ isnot(radios[2].selected, true, "Correct radio button should be selected");
+ EventUtils.synthesizeMouseAtCenter(radios[0], { clickCount: 1 }, gManagerWindow);
+ is(Services.prefs.getCharPref("extensions.inlinesettings3.radioString"), "india", "Radio pref should have been updated");
+ EventUtils.synthesizeMouseAtCenter(radios[2], { clickCount: 1 }, gManagerWindow);
+ is(Preferences.get("extensions.inlinesettings3.radioString", "wrong"), "kilo \u338F", "Radio pref should have been updated");
+
+ ok(!settings[3].hasAttribute("first-row"), "Not the first row");
+ Services.prefs.setIntPref("extensions.inlinesettings3.menulist", 8);
+ var input = settings[3].firstElementChild;
+ is(input.value, "8", "Menulist should have initial value");
+ input.focus();
+ EventUtils.synthesizeKey("n", {}, gManagerWindow);
+ is(input.value, "9", "Menulist should have updated value");
+ is(Services.prefs.getIntPref("extensions.inlinesettings3.menulist"), 9, "Menulist pref should have been updated");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with inline preferences as optionsURL
+add_test(function() {
+ observer.checkHidden("inlinesettings3@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings2@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 5, "Grid should have settings children");
+
+ var node = settings[0];
+ node = settings[0];
+ is_element_hidden(node, "Unsupported settings should not be visible");
+ ok(!node.hasAttribute("first-row"), "Hidden row is not the first row");
+
+ node = settings[1];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(node.hasAttribute("first-row"), "First visible row should have first-row attribute");
+ var description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "Description Attribute", "Description node should contain description");
+
+ node = settings[2];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(!node.hasAttribute("first-row"), "Not the first row");
+ description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "Description Text Node", "Description node should contain description");
+
+ node = settings[3];
+ is(node.nodeName, "setting", "Should be a setting node");
+ ok(!node.hasAttribute("first-row"), "Not the first row");
+ description = gManagerWindow.document.getAnonymousElementByAttribute(node, "class", "preferences-description");
+ is(description.textContent, "This is a test, all this text should be visible", "Description node should contain description");
+ var button = node.firstElementChild;
+ isnot(button, null, "There should be a button");
+
+ node = settings[4];
+ is_element_hidden(node, "Unsupported settings should not be visible");
+ ok(!node.hasAttribute("first-row"), "Hidden row is not the first row");
+
+ button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with non-inline preferences as optionsURL
+add_test(function() {
+ observer.checkHidden("inlinesettings2@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "noninlinesettings@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkNotDisplayed();
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ var button = gManagerWindow.document.getElementById("detail-prefs-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
+
+// Addon with options.xul, disabling and enabling should hide and show settings UI
+add_test(function() {
+ observer.checkNotHidden();
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+ is(gManagerWindow.gViewController.currentViewId,
+ "addons://detail/inlinesettings1%40tests.mozilla.org",
+ "Current view should not scroll to preferences");
+ checkScrolling(false);
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ // disable
+ var button = gManagerWindow.document.getElementById("detail-disable-btn");
+ button.focus(); // make sure it's in view
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ gCategoryUtilities.openType("extension", function() {
+ var addon = get_addon_element(gManagerWindow, "inlinesettings1@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, 0, "Grid should not have settings children");
+
+ // enable
+ var button = gManagerWindow.document.getElementById("detail-enable-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ observer.callback = function() {
+ observer.checkDisplayed("inlinesettings1@tests.mozilla.org");
+
+ settings = grid.querySelectorAll("rows > setting");
+ is(settings.length, SETTINGS_ROWS, "Grid should have settings children");
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ };
+ });
+ });
+ });
+});
+
+
+// Addon with options.xul that requires a restart to disable,
+// disabling and enabling should not hide and show settings UI.
+add_test(function() {
+ observer.checkHidden("inlinesettings1@tests.mozilla.org");
+
+ var addon = get_addon_element(gManagerWindow, "inlinesettings2@tests.mozilla.org");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ observer.checkDisplayed("inlinesettings2@tests.mozilla.org");
+
+ var grid = gManagerWindow.document.getElementById("detail-grid");
+ var settings = grid.querySelectorAll("rows > setting");
+ ok(settings.length > 0, "Grid should have settings children");
+
+ // disable
+ var button = gManagerWindow.document.getElementById("detail-disable-btn");
+ button.focus(); // make sure it's in view
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ observer.checkNotHidden();
+
+ settings = grid.querySelectorAll("rows > setting");
+ ok(settings.length > 0, "Grid should still have settings children");
+
+ // cancel pending disable
+ button = gManagerWindow.document.getElementById("detail-enable-btn");
+ button.focus(); // make sure it's in view
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+ observer.checkNotDisplayed();
+
+ gCategoryUtilities.openType("extension", run_next_test);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_install.js b/toolkit/mozapps/extensions/test/browser/browser_install.js
new file mode 100644
index 000000000..880a4624d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_install.js
@@ -0,0 +1,312 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests tha installs and undoing installs show up correctly
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gSearchCount = 0;
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+ Services.prefs.setCharPref("extensions.getAddons.search.url", TESTROOT + "browser_install.xml");
+ // Allow http update checks
+ Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ Services.prefs.clearUserPref("extensions.checkUpdateSecurity");
+
+ AddonManager.getAddonByID("install1@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+ finish();
+ });
+ });
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function installAddon(aCallback) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_install1_2.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function installUpgrade(aCallback) {
+ AddonManager.getAddonByID("install1@tests.mozilla.org", function(aAddon) {
+ aAddon.findUpdates({
+ onUpdateAvailable: function(aAddon, aInstall) {
+ is(get_list_item_count(), 1, "Should be only one item in the list");
+
+ aInstall.addListener({
+ onDownloadEnded: function() {
+ is(get_list_item_count(), 1, "Should be only one item in the list once the update has started");
+ },
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ aInstall.install();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+function cancelInstall(aCallback) {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_install1_2.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onDownloadEnded: function(aInstall) {
+ executeSoon(function() {
+ aInstall.cancel();
+ aCallback();
+ });
+ return false;
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function installSearchResult(aCallback) {
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ // Search for something different each time
+ searchBox.value = "foo" + gSearchCount;
+ gSearchCount++;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ let remote = gManagerWindow.document.getElementById("search-filter-remote")
+ EventUtils.synthesizeMouseAtCenter(remote, { }, gManagerWindow);
+
+ let item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should see the search result in the list");
+
+ let status = get_node(item, "install-status");
+ EventUtils.synthesizeMouseAtCenter(get_node(status, "install-remote-btn"), {}, gManagerWindow);
+
+ item.mInstall.addListener({
+ onInstallEnded: function() {
+ executeSoon(aCallback);
+ }
+ });
+ });
+}
+
+function get_list_item_count() {
+ return get_test_items_in_list(gManagerWindow).length;
+}
+
+function check_undo_install() {
+ is(get_list_item_count(), 1, "Should be only one item in the list");
+
+ let item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should see the pending install in the list");
+ // Force XBL to apply
+ item.clientTop;
+ is_element_visible(get_node(item, "pending"), "Pending message should be visible");
+ is(get_node(item, "pending").textContent, "Install Tests will be installed after you restart " + gApp + ".", "Pending message should be correct");
+
+ EventUtils.synthesizeMouseAtCenter(get_node(item, "undo-btn"), {}, gManagerWindow);
+
+ is(get_list_item_count(), 0, "Should be no items in the list");
+
+ item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!item, "Should no longer see the pending install");
+}
+
+function check_undo_upgrade() {
+ is(get_list_item_count(), 1, "Should be only one item in the list");
+
+ let item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should see the pending upgrade in the list");
+ // Force XBL to apply
+ item.clientTop;
+ is_element_visible(get_node(item, "pending"), "Pending message should be visible");
+ is(get_node(item, "pending").textContent, "Install Tests will be updated after you restart " + gApp + ".", "Pending message should be correct");
+
+ EventUtils.synthesizeMouseAtCenter(get_node(item, "undo-btn"), {}, gManagerWindow);
+
+ is(get_list_item_count(), 1, "Should be only one item in the list");
+
+ item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should still see installed item in the list");
+ is_element_hidden(get_node(item, "pending"), "Pending message should be hidden");
+}
+
+// Install an add-on through the API with the manager open
+add_test(function() {
+ gCategoryUtilities.openType("extension", function() {
+ installAddon(function() {
+ check_undo_install();
+ run_next_test();
+ });
+ });
+});
+
+// Install an add-on with the manager closed then open it
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ installAddon(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_undo_install();
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Install an add-on through the search page and then undo it
+add_test(function() {
+ installSearchResult(function() {
+ check_undo_install();
+ run_next_test();
+ });
+});
+
+// Install an add-on through the search page then switch to the extensions page
+// and then undo it
+add_test(function() {
+ installSearchResult(function() {
+ gCategoryUtilities.openType("extension", function() {
+ check_undo_install();
+ run_next_test();
+ });
+ });
+});
+
+// Install an add-on through the search page then re-open the manager and then
+// undo it
+add_test(function() {
+ installSearchResult(function() {
+ close_manager(gManagerWindow, function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_undo_install();
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Cancel an install after download with the manager open
+add_test(function() {
+ cancelInstall(function() {
+ is(get_list_item_count(), 0, "Should be no items in the list");
+
+ run_next_test();
+ });
+});
+
+// Cancel an install after download with the manager closed
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ cancelInstall(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(get_list_item_count(), 0, "Should be no items in the list");
+
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Install an existing add-on for the subsequent tests
+add_test(function() {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_install1_1.xpi",
+ function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: run_next_test
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+});
+
+// Install an upgrade through the API with the manager open
+add_test(function() {
+ installAddon(function() {
+ check_undo_upgrade();
+ run_next_test();
+ });
+});
+
+// Install an upgrade through the API with the manager open
+add_test(function() {
+ installUpgrade(function() {
+ check_undo_upgrade();
+ run_next_test();
+ });
+});
+
+// Install an upgrade through the API with the manager closed
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ installAddon(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ check_undo_upgrade();
+ run_next_test();
+ });
+ });
+ });
+});
+
+// Cancel an upgrade after download with the manager open
+add_test(function() {
+ cancelInstall(function() {
+ is(get_list_item_count(), 1, "Should be no items in the list");
+ let item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should still see installed item in the list");
+ is_element_hidden(get_node(item, "pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Cancel an upgrade after download with the manager closed
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ cancelInstall(function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(get_list_item_count(), 1, "Should be no items in the list");
+ let item = get_addon_element(gManagerWindow, "install1@tests.mozilla.org");
+ ok(!!item, "Should still see installed item in the list");
+ is_element_hidden(get_node(item, "pending"), "Pending message should be hidden");
+
+ run_next_test();
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_install.rdf b/toolkit/mozapps/extensions/test/browser/browser_install.rdf
new file mode 100644
index 000000000..437bf9b85
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_install.rdf
@@ -0,0 +1,27 @@
+<?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:extension:install1@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/browser_install1_3.xpi</em:updateLink>
+ <em:updateHash>sha1:4f0f4391914e3e036beca50cbac33958e5269643</em:updateHash>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_install.rdf^headers^ b/toolkit/mozapps/extensions/test/browser/browser_install.rdf^headers^
new file mode 100644
index 000000000..2e4f8163b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_install.rdf^headers^
@@ -0,0 +1 @@
+Connection: close
diff --git a/toolkit/mozapps/extensions/test/browser/browser_install.xml b/toolkit/mozapps/extensions/test/browser/browser_install.xml
new file mode 100644
index 000000000..0643c811a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_install.xml
@@ -0,0 +1,34 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1">
+ <addon>
+ <name>Install Tests</name>
+ <type id='1'>Extension</type>
+ <guid>install1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test add-on</summary>
+ <description>Test add-on</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="2">http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_install1_3.xpi b/toolkit/mozapps/extensions/test/browser/browser_install1_3.xpi
new file mode 100644
index 000000000..de3c90353
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_install1_3.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/browser_installssl.js b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
new file mode 100644
index 000000000..b0726ef9e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_installssl.js
@@ -0,0 +1,374 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const xpi = RELATIVE_DIR + "addons/browser_installssl.xpi";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const NETWORK_FAILURE = AddonManager.ERROR_NETWORK_FAILURE;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+var gPendingInstall = null;
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ registerCleanupFunction(function() {
+ var cos = Cc["@mozilla.org/security/certoverride;1"].
+ getService(Ci.nsICertOverrideService);
+ cos.clearValidityOverride("nocert.example.com", -1);
+ cos.clearValidityOverride("self-signed.example.com", -1);
+ cos.clearValidityOverride("untrusted.example.com", -1);
+ cos.clearValidityOverride("expired.example.com", -1);
+
+ try {
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+ }
+ catch (e) {
+ }
+
+ if (gPendingInstall) {
+ gTests = [];
+ ok(false, "Timed out in the middle of downloading " + gPendingInstall.sourceURI.spec);
+ try {
+ gPendingInstall.cancel();
+ }
+ catch (e) {
+ }
+ }
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_install_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_install_tests(callback) {
+ function run_next_install_test() {
+ if (gTests.length == 0) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + xpi;
+ var message = "Should have seen the right result for an install redirected from " +
+ mainURL + " to " + redirectURL;
+ }
+ else {
+ url = mainURL + xpi;
+ message = "Should have seen the right result for an install from " +
+ mainURL;
+ }
+
+ AddonManager.getInstallForURL(url, function(install) {
+ gPendingInstall = install;
+ install.addListener({
+ onDownloadEnded: function(install) {
+ is(SUCCESS, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ // Don't proceed with the install
+ install.cancel();
+ gPendingInstall = null;
+ run_next_install_test();
+ return false;
+ },
+
+ onDownloadFailed: function(install) {
+ is(install.error, expectedStatus, message);
+ info("Install test ran in " + (Date.now() - gLast) + "ms");
+ gPendingInstall = null;
+ run_next_install_test();
+ }
+ });
+ install.install();
+ }, "application/x-xpinstall");
+ }
+
+ run_next_install_test();
+}
+
+// Add overrides for the bad certificates
+function addCertOverrides() {
+ addCertOverride("nocert.example.com", Ci.nsICertOverrideService.ERROR_MISMATCH);
+ addCertOverride("self-signed.example.com", Ci.nsICertOverrideService.ERROR_UNTRUSTED);
+ addCertOverride("untrusted.example.com", Ci.nsICertOverrideService.ERROR_UNTRUSTED);
+ addCertOverride("expired.example.com", Ci.nsICertOverrideService.ERROR_TIME);
+}
+
+// Runs tests with built-in certificates required, no certificate exceptions
+// and no hashes
+add_test(function() {
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, no certificate
+// exceptions and no hashes
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTP, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTP, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTP, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests with built-in certificates required, all certificate exceptions
+// and no hashes
+add_test(function() {
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+ addCertOverrides();
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, NETWORK_FAILURE);
+ add_install_test(NOCERT, null, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, null, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, null, NETWORK_FAILURE);
+ add_install_test(EXPIRED, null, NETWORK_FAILURE);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, NETWORK_FAILURE);
+ add_install_test(HTTPS, NOCERT, NETWORK_FAILURE);
+ add_install_test(HTTPS, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(HTTPS, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(HTTPS, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, NETWORK_FAILURE);
+ add_install_test(NOCERT, NOCERT, NETWORK_FAILURE);
+ add_install_test(NOCERT, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(NOCERT, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(NOCERT, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, NOCERT, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, NOCERT, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, EXPIRED, NETWORK_FAILURE);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, NETWORK_FAILURE);
+ add_install_test(EXPIRED, NOCERT, NETWORK_FAILURE);
+ add_install_test(EXPIRED, SELFSIGNED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, UNTRUSTED, NETWORK_FAILURE);
+ add_install_test(EXPIRED, EXPIRED, NETWORK_FAILURE);
+
+ run_install_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates, all certificate
+// exceptions and no hashes
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ // Tests that a simple install works as expected.
+ add_install_test(HTTP, null, SUCCESS);
+ add_install_test(HTTPS, null, SUCCESS);
+ add_install_test(NOCERT, null, SUCCESS);
+ add_install_test(SELFSIGNED, null, SUCCESS);
+ add_install_test(UNTRUSTED, null, SUCCESS);
+ add_install_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_install_test(HTTP, HTTP, SUCCESS);
+ add_install_test(HTTP, HTTPS, SUCCESS);
+ add_install_test(HTTP, NOCERT, SUCCESS);
+ add_install_test(HTTP, SELFSIGNED, SUCCESS);
+ add_install_test(HTTP, UNTRUSTED, SUCCESS);
+ add_install_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_install_test(HTTPS, HTTP, NETWORK_FAILURE);
+ add_install_test(HTTPS, HTTPS, SUCCESS);
+ add_install_test(HTTPS, NOCERT, SUCCESS);
+ add_install_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_install_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_install_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_install_test(NOCERT, HTTP, NETWORK_FAILURE);
+ add_install_test(NOCERT, HTTPS, SUCCESS);
+ add_install_test(NOCERT, NOCERT, SUCCESS);
+ add_install_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_install_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_install_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_install_test(SELFSIGNED, HTTP, NETWORK_FAILURE);
+ add_install_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_install_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_install_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_install_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_install_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_install_test(UNTRUSTED, HTTP, NETWORK_FAILURE);
+ add_install_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_install_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_install_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_install_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_install_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_install_test(EXPIRED, HTTP, NETWORK_FAILURE);
+ add_install_test(EXPIRED, HTTPS, SUCCESS);
+ add_install_test(EXPIRED, NOCERT, SUCCESS);
+ add_install_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_install_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_install_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_install_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_list.js b/toolkit/mozapps/extensions/test/browser/browser_list.js
new file mode 100644
index 000000000..49427329f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_list.js
@@ -0,0 +1,956 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the list view
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", tempScope);
+var LightweightThemeManager = tempScope.LightweightThemeManager;
+const { REQUIRE_SIGNING } = Components.utils.import("resource://gre/modules/addons/AddonConstants.jsm", {});
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+var gVersion = Services.appinfo.version;
+var gBlocklistURL = Services.urlFormatter.formatURLPref("extensions.blocklist.detailsURL");
+var gDate = new Date(2010, 7, 16);
+var infoURL = Services.urlFormatter.formatURLPref("app.support.baseURL") + "unsigned-addons";
+
+const EXPECTED_ADDONS = 13;
+
+var gLWTheme = {
+ id: "4",
+ version: "1",
+ name: "Bling",
+ description: "SO MUCH BLING!",
+ author: "Pixel Pusher",
+ homepageURL: "http://mochi.test:8888/data/index.html",
+ headerURL: "http://mochi.test:8888/data/header.png",
+ footerURL: "http://mochi.test:8888/data/footer.png",
+ previewURL: "http://mochi.test:8888/data/preview.png",
+ iconURL: "http://mochi.test:8888/data/icon.png"
+ };
+
+add_task(function*() {
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on",
+ version: "1.0",
+ description: "A test add-on",
+ longDescription: " A longer description",
+ updateDate: gDate
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "2.0",
+ longDescription: " A longer description",
+ _userDisabled: true,
+ isActive: false,
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ longDescription: " A longer description",
+ isActive: false,
+ isCompatible: false,
+ appDisabled: true,
+ permissions: AddonManager.PERM_CAN_ENABLE |
+ AddonManager.PERM_CAN_DISABLE |
+ AddonManager.PERM_CAN_UPGRADE
+ }, {
+ id: "addon4@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon4@tests.mozilla.org",
+ name: "Test add-on 4",
+ _userDisabled: true,
+ isActive: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_SOFTBLOCKED
+ }, {
+ id: "addon5@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon5@tests.mozilla.org",
+ name: "Test add-on 5",
+ isActive: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_BLOCKED,
+ appDisabled: true
+ }, {
+ id: "addon6@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon6@tests.mozilla.org",
+ name: "Test add-on 6",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon7@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon7@tests.mozilla.org",
+ name: "Test add-on 7",
+ blocklistState: Ci.nsIBlocklistService.STATE_OUTDATED,
+ }, {
+ id: "addon8@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon8@tests.mozilla.org",
+ name: "Test add-on 8",
+ blocklistState: Ci.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE,
+ }, {
+ id: "addon9@tests.mozilla.org",
+ blocklistURL: "http://example.com/addon9@tests.mozilla.org",
+ name: "Test add-on 9",
+ blocklistState: Ci.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE,
+ }, {
+ id: "addon10@tests.mozilla.org",
+ name: "Test add-on 10",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ }, {
+ id: "addon11@tests.mozilla.org",
+ name: "Test add-on 11",
+ signedState: AddonManager.SIGNEDSTATE_MISSING,
+ isActive: false,
+ isCompatible: false,
+ appDisabled: true,
+ }, {
+ id: "addon12@tests.mozilla.org",
+ name: "Test add-on 12",
+ signedState: AddonManager.SIGNEDSTATE_PRELIMINARY,
+ foreignInstall: true,
+ }, {
+ id: "addon13@tests.mozilla.org",
+ name: "Test add-on 13",
+ signedState: AddonManager.SIGNEDSTATE_SIGNED,
+ foreignInstall: true,
+ }, {
+ id: "addon15@tests.mozilla.org",
+ name: "Test add-on 15",
+ hidden: true,
+ }]);
+
+ gManagerWindow = yield open_manager(null);
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+});
+
+function get_test_items() {
+ var tests = "@tests.mozilla.org";
+
+ var items = {};
+ var item = gManagerWindow.document.getElementById("addon-list").firstChild;
+
+ while (item) {
+ if (item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests &&
+ !is_hidden(item))
+ items[item.mAddon.name] = item;
+ item = item.nextSibling;
+ }
+
+ return items;
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function get_class_node(parent, cls) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "class", cls);
+}
+
+// Check that the list appears to have displayed correctly and trigger some
+// changes
+add_task(function*() {
+ yield gCategoryUtilities.openType("extension");
+ let items = get_test_items();
+ is(Object.keys(items).length, EXPECTED_ADDONS, "Should be the right number of add-ons installed");
+
+ info("Addon 1");
+ let addon = items["Test add-on"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ let { name, version } = yield get_tooltip_info(addon);
+ is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
+ is(name, "Test add-on", "Tooltip name should be correct");
+ is(version, "1.0", "Tooltip version should be correct");
+ is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
+ is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+ is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+ is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Disabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Addon 2");
+ addon = items["Test add-on 2"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
+ is(name, "Test add-on 2", "Tooltip name should be correct");
+ is(version, "2.0", "Tooltip version should be correct");
+ is_element_hidden(get_node(addon, "description"), "Description should be hidden");
+ is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+ is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+ is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Enabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Addon 3");
+ addon = items["Test add-on 3"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 3", "Name should be correct");
+ is(name, "Test add-on 3", "Tooltip name should be correct");
+ is(version, undefined, "Tooltip version should be hidden");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_hidden(get_node(addon, "remove-btn"), "Remove button should be hidden");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+ is(get_node(addon, "warning").textContent, "Test add-on 3 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 4");
+ addon = items["Test add-on 4"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+ is(name, "Test add-on 4", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+ is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+ is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+ is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+ is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Enabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Addon 5");
+ addon = items["Test add-on 5"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 5", "Name should be correct");
+ is(name, "Test add-on 5", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_visible(get_node(addon, "error"), "Error message should be visible");
+ is(get_node(addon, "error").textContent, "Test add-on 5 has been disabled due to security or stability issues.", "Error message should be correct");
+ is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+ is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+ is(get_node(addon, "error-link").href, "http://example.com/addon5@tests.mozilla.org", "Error link should be correct");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 6");
+ addon = items["Test add-on 6"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
+ is(name, "Test add-on 6", "Tooltip name should be correct");
+ is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Disabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+ is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 7");
+ addon = items["Test add-on 7"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
+ is(name, "Test add-on 7", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
+ is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
+ is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+ is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
+ is(get_node(addon, "warning-link").href, "http://example.com/addon7@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Disabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "disable-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Addon 8");
+ addon = items["Test add-on 8"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 8", "Name should be correct");
+ is(name, "Test add-on 8", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_visible(get_node(addon, "error"), "Error message should be visible");
+ is(get_node(addon, "error").textContent, "Test add-on 8 is known to be vulnerable and should be updated.", "Error message should be correct");
+ is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+ is(get_node(addon, "error-link").value, "Update Now", "Error link text should be correct");
+ is(get_node(addon, "error-link").href, "http://example.com/addon8@tests.mozilla.org", "Error link should be correct");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 9");
+ addon = items["Test add-on 9"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 9", "Name should be correct");
+ is(name, "Test add-on 9", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_visible(get_node(addon, "error"), "Error message should be visible");
+ is(get_node(addon, "error").textContent, "Test add-on 9 is known to be vulnerable. Use with caution.", "Error message should be correct");
+ is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+ is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+ is(get_node(addon, "error-link").href, "http://example.com/addon9@tests.mozilla.org", "Error link should be correct");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ // These tests are only appropriate when signing can be turned off
+ if (!REQUIRE_SIGNING) {
+ info("Addon 10");
+ addon = items["Test add-on 10"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 10", "Name should be correct");
+ is(name, "Test add-on 10", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+ is(get_node(addon, "warning").textContent, "Test add-on 10 could not be verified for use in " + gApp + ". Proceed with caution.", "Warning message should be correct");
+ is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+ is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+ is(get_node(addon, "warning-link").href, infoURL, "Warning link should be correct");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 11");
+ addon = items["Test add-on 11"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 11", "Name should be correct");
+ is(name, "Test add-on 11", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+ is(get_node(addon, "warning").textContent, "Test add-on 11 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Filter for disabled unsigned extensions shouldn't appear because signing checks are off");
+ let filterButton = gManagerWindow.document.getElementById("show-disabled-unsigned-extensions");
+ let showAllButton = gManagerWindow.document.getElementById("show-all-extensions");
+ let signingInfoUI = gManagerWindow.document.getElementById("disabled-unsigned-addons-info");
+ is_element_hidden(filterButton, "Button for showing disabled unsigned extensions should be hidden");
+ is_element_hidden(showAllButton, "Button for showing all extensions should be hidden");
+ is_element_hidden(signingInfoUI, "Signing info UI should be hidden");
+ }
+});
+
+// Check the add-ons are now in the right state
+add_task(function*() {
+ let [a1, a2, a4, a6] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon6@tests.mozilla.org"]);
+
+ is(a1.pendingOperations, AddonManager.PENDING_DISABLE, "Add-on 1 should be pending disable");
+ is(a2.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 2 should be pending enable");
+ is(a4.pendingOperations, AddonManager.PENDING_ENABLE, "Add-on 4 should be pending enable");
+});
+
+// Reload the list to make sure the changes are still pending and that undoing
+// works
+add_task(function*() {
+ yield gCategoryUtilities.openType("plugin");
+ yield gCategoryUtilities.openType("extension");
+
+ let items = get_test_items();
+ is(Object.keys(items).length, EXPECTED_ADDONS, "Should be the right number of add-ons installed");
+
+ info("Addon 1");
+ let addon = items["Test add-on"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ let { name, version } = yield get_tooltip_info(addon);
+ is(get_node(addon, "name").value, "Test add-on", "Name should be correct");
+ is(name, "Test add-on", "Tooltip name should be correct");
+ is(version, "1.0", "Tooltip version should be correct");
+ is_element_visible(get_node(addon, "description"), "Description should be visible");
+ is(get_node(addon, "description").value, "A test add-on", "Description should be correct");
+ is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+ is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+ is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Undoing");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 2");
+ addon = items["Test add-on 2"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 2", "Name should be correct");
+ is(name, "Test add-on 2", "Tooltip name should be correct");
+ is(version, "2.0", "Tooltip version should be correct");
+ is_element_hidden(get_node(addon, "description"), "Description should be hidden");
+ is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+ is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+ is(get_node(addon, "date-updated").value, "Unknown", "Date should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 2 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Undoing");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 4");
+ addon = items["Test add-on 4"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 4", "Name should be correct");
+ is(name, "Test add-on 4", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 4 will be enabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Undoing");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be visible");
+ is(get_node(addon, "warning").textContent, "Test add-on 4 is known to cause security or stability issues.", "Warning message should be correct");
+ is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+ is(get_node(addon, "warning-link").value, "More Information", "Warning link text should be correct");
+ is(get_node(addon, "warning-link").href, "http://example.com/addon4@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 6");
+ addon = items["Test add-on 6"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 6", "Name should be correct");
+ is(name, "Test add-on 6", "Tooltip name should be correct");
+ is_element_visible(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be visible");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Enabling");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "enable-btn"), {}, gManagerWindow);
+ is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be visible");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+
+ info("Addon 7");
+ addon = items["Test add-on 7"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 7", "Name should be correct");
+ is(name, "Test add-on 7", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be visible");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be visible");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_visible(get_node(addon, "pending"), "Pending message should be visible");
+ is(get_node(addon, "pending").textContent, "Test add-on 7 will be disabled after you restart " + gApp + ".", "Pending message should be correct");
+
+ info("Undoing");
+ EventUtils.synthesizeMouseAtCenter(get_node(addon, "undo-btn"), {}, gManagerWindow);
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_visible(get_node(addon, "warning"), "Warning message should be hidden");
+ is(get_node(addon, "warning").textContent, "An important update is available for Test add-on 7.", "Warning message should be correct");
+ is_element_visible(get_node(addon, "warning-link"), "Warning link should be visible");
+ is(get_node(addon, "warning-link").value, "Update Now", "Warning link text should be correct");
+ is(get_node(addon, "warning-link").href, "http://example.com/addon7@tests.mozilla.org", "Warning link should be correct");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+});
+
+// Check the add-ons are now in the right state
+add_task(function*() {
+ let [a1, a2, a4] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon4@tests.mozilla.org"]);
+
+ is(a1.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+ is(a2.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+ is(a4.pendingOperations, 0, "Add-on 1 should not have any pending operations");
+});
+
+// Check that upgrades with onExternalInstall take effect immediately
+add_task(function*() {
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on replacement",
+ version: "2.0",
+ description: "A test add-on with a new description",
+ updateDate: gDate,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon14@tests.mozilla.org",
+ name: "Test add-on 14",
+ hidden: true,
+ }]);
+
+ let items = get_test_items();
+ is(Object.keys(items).length, EXPECTED_ADDONS, "Should be the right number of add-ons installed");
+
+ let addon = items["Test add-on replacement"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ let { name, version } = yield get_tooltip_info(addon);
+ is(get_node(addon, "name").value, "Test add-on replacement", "Name should be correct");
+ is(name, "Test add-on replacement", "Tooltip name should be correct");
+ is(version, "2.0", "Tooltip version should be correct");
+ is_element_visible(get_node(addon, "description"), "Description should be visible");
+ is(get_node(addon, "description").value, "A test add-on with a new description", "Description should be correct");
+ is_element_hidden(get_class_node(addon, "disabled-postfix"), "Disabled postfix should be hidden");
+ is_element_hidden(get_class_node(addon, "update-postfix"), "Update postfix should be hidden");
+ is(get_node(addon, "date-updated").value, formatDate(gDate), "Update date should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+ is_element_hidden(get_node(addon, "pending"), "Pending message should be hidden");
+});
+
+// Check that focus changes correctly move around the selected list item
+add_task(function*() {
+ function is_node_in_list(aNode) {
+ var list = gManagerWindow.document.getElementById("addon-list");
+
+ while (aNode && aNode != list)
+ aNode = aNode.parentNode;
+
+ if (aNode)
+ return true;
+ return false;
+ }
+
+ // Ignore the OSX full keyboard access setting
+ Services.prefs.setBoolPref("accessibility.tabfocus_applies_to_xul", false);
+
+ let items = get_test_items();
+
+ var fm = Cc["@mozilla.org/focus-manager;1"].
+ getService(Ci.nsIFocusManager);
+
+ let addon = items["Test add-on 6"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ EventUtils.synthesizeMouseAtCenter(addon, { }, gManagerWindow);
+ is(fm.focusedElement, addon.parentNode, "Focus should have moved to the list");
+
+ EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
+ is(fm.focusedElement, get_node(addon, "details-btn"), "Focus should have moved to the more button");
+
+ EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
+ is(fm.focusedElement, get_node(addon, "disable-btn"), "Focus should have moved to the disable button");
+
+ EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
+ is(fm.focusedElement, get_node(addon, "remove-btn"), "Focus should have moved to the remove button");
+
+ EventUtils.synthesizeKey("VK_TAB", { }, gManagerWindow);
+ ok(!is_node_in_list(fm.focusedElement), "Focus should be outside the list");
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
+ is(fm.focusedElement, get_node(addon, "remove-btn"), "Focus should have moved to the remove button");
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
+ is(fm.focusedElement, get_node(addon, "details-btn"), "Focus should have moved to the more button");
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
+ is(fm.focusedElement, addon.parentNode, "Focus should have moved to the list");
+
+ EventUtils.synthesizeKey("VK_TAB", { shiftKey: true }, gManagerWindow);
+ ok(!is_node_in_list(fm.focusedElement), "Focus should be outside the list");
+
+ try {
+ Services.prefs.clearUserPref("accessibility.tabfocus_applies_to_xul");
+ }
+ catch (e) { }
+});
+
+
+add_task(function*() {
+ info("Enabling lightweight theme");
+ LightweightThemeManager.currentTheme = gLWTheme;
+
+ gManagerWindow.loadView("addons://list/theme");
+ yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+
+ var addon = get_addon_element(gManagerWindow, "4@personas.mozilla.org");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ info("Disabling lightweight theme");
+ LightweightThemeManager.currentTheme = null;
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_visible(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ let [aAddon] = yield promiseAddonsByIDs(["4@personas.mozilla.org"]);
+ aAddon.uninstall();
+});
+
+// Check that onPropertyChanges for appDisabled updates the UI
+add_task(function*() {
+ info("Checking that onPropertyChanges for appDisabled updates the UI");
+
+ let [aAddon] = yield promiseAddonsByIDs(["addon2@tests.mozilla.org"]);
+ aAddon.userDisabled = true;
+ aAddon.isCompatible = true;
+ aAddon.appDisabled = false;
+
+ gManagerWindow.loadView("addons://list/extension");
+ yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+ var el = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+
+ is(el.getAttribute("active"), "false", "Addon should not be marked as active");
+ is_element_hidden(get_node(el, "warning"), "Warning message should not be visible");
+
+ info("Making addon incompatible and appDisabled");
+ aAddon.isCompatible = false;
+ aAddon.appDisabled = true;
+
+ is(el.getAttribute("active"), "false", "Addon should not be marked as active");
+ is_element_visible(get_node(el, "warning"), "Warning message should be visible");
+ is(get_node(el, "warning").textContent, "Test add-on 2 is incompatible with " + gApp + " " + gVersion + ".", "Warning message should be correct");
+});
+
+// Check that the list displays correctly when signing is required
+add_task(function*() {
+ yield close_manager(gManagerWindow);
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+ gManagerWindow = yield open_manager(null);
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ yield gCategoryUtilities.openType("extension");
+ let items = get_test_items();
+ is(Object.keys(items).length, EXPECTED_ADDONS, "Should be the right number of add-ons installed");
+
+ info("Addon 10");
+ let addon = items["Test add-on 10"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ let { name, version } = yield get_tooltip_info(addon);
+ is(get_node(addon, "name").value, "Test add-on 10", "Name should be correct");
+ is(name, "Test add-on 10", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_visible(get_node(addon, "error"), "Error message should be visible");
+ is(get_node(addon, "error").textContent, "Test add-on 10 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
+ is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+ is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+ is(get_node(addon, "error-link").href, infoURL, "Error link should be correct");
+
+ info("Addon 11");
+ addon = items["Test add-on 11"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 11", "Name should be correct");
+ is(name, "Test add-on 11", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_hidden(get_node(addon, "disable-btn"), "Disable button should be hidden");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_visible(get_node(addon, "error"), "Error message should be visible");
+ is(get_node(addon, "error").textContent, "Test add-on 11 could not be verified for use in " + gApp + " and has been disabled.", "Error message should be correct");
+ is_element_visible(get_node(addon, "error-link"), "Error link should be visible");
+ is(get_node(addon, "error-link").value, "More Information", "Error link text should be correct");
+ is(get_node(addon, "error-link").href, infoURL, "Error link should be correct");
+
+ info("Addon 12");
+ addon = items["Test add-on 12"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon))
+ is(get_node(addon, "name").value, "Test add-on 12", "Name should be correct");
+ is(name, "Test add-on 12", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+ is_element_hidden(get_node(addon, "error-link"), "Error link should be hidden");
+
+ info("Addon 13");
+ addon = items["Test add-on 13"];
+ addon.parentNode.ensureElementIsVisible(addon);
+ ({ name, version } = yield get_tooltip_info(addon));
+ is(get_node(addon, "name").value, "Test add-on 13", "Name should be correct");
+ is(name, "Test add-on 13", "Tooltip name should be correct");
+
+ is_element_hidden(get_node(addon, "preferences-btn"), "Preferences button should be hidden");
+ is_element_hidden(get_node(addon, "enable-btn"), "Enable button should be hidden");
+ is_element_visible(get_node(addon, "disable-btn"), "Disable button should be visible");
+ is_element_visible(get_node(addon, "remove-btn"), "Remove button should be visible");
+
+ is_element_hidden(get_node(addon, "warning"), "Warning message should be hidden");
+ is_element_hidden(get_node(addon, "warning-link"), "Warning link should be hidden");
+ is_element_hidden(get_node(addon, "error"), "Error message should be hidden");
+
+ info("Filter for disabled unsigned extensions");
+ let filterButton = gManagerWindow.document.getElementById("show-disabled-unsigned-extensions");
+ let showAllButton = gManagerWindow.document.getElementById("show-all-extensions");
+ let signingInfoUI = gManagerWindow.document.getElementById("disabled-unsigned-addons-info");
+ is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible");
+ is_element_hidden(showAllButton, "Button for showing all extensions should be hidden");
+ is_element_hidden(signingInfoUI, "Signing info UI should be hidden");
+
+ filterButton.click();
+
+ yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+
+ is_element_hidden(filterButton, "Button for showing disabled unsigned extensions should be hidden");
+ is_element_visible(showAllButton, "Button for showing all extensions should be visible");
+ is_element_visible(signingInfoUI, "Signing info UI should be visible");
+
+ items = get_test_items();
+ is(Object.keys(items).length, 2, "Two add-ons should be shown");
+ is(Object.keys(items)[0], "Test add-on 10", "The disabled unsigned extension should be shown");
+ is(Object.keys(items)[1], "Test add-on 11", "The disabled unsigned extension should be shown");
+
+ showAllButton.click();
+
+ yield new Promise(resolve => wait_for_view_load(gManagerWindow, resolve));
+
+ items = get_test_items();
+ is(Object.keys(items).length, EXPECTED_ADDONS, "All add-ons should be shown again");
+ is_element_visible(filterButton, "Button for showing disabled unsigned extensions should be visible again");
+ is_element_hidden(showAllButton, "Button for showing all extensions should be hidden again");
+ is_element_hidden(signingInfoUI, "Signing info UI should be hidden again");
+
+ Services.prefs.setBoolPref("xpinstall.signatures.required", false);
+});
+
+add_task(function*() {
+ return close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js b/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
new file mode 100644
index 000000000..0c5eb2da6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_manualupdates.js
@@ -0,0 +1,246 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests manual updates, including the Available Updates pane
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+var gAvailableCategory;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "auto updating addon",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_ENABLE
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+
+add_test(function() {
+ gAvailableCategory = gManagerWindow.gCategories.get("addons://updates/available");
+ is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should initially be hidden");
+
+ gProvider.createAddons([{
+ id: "addon2@tests.mozilla.org",
+ name: "manually updating addon",
+ version: "1.0",
+ isCompatible: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_BLOCKED,
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }]);
+
+ is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should still be hidden");
+
+ run_next_test();
+});
+
+
+add_test(function() {
+ let finished = 0;
+ function maybeRunNext() {
+ if (++finished == 2)
+ run_next_test();
+ }
+
+ gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
+ gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
+ is(gCategoryUtilities.isVisible(gAvailableCategory), true, "Available Updates category should now be visible");
+ is(gAvailableCategory.badgeCount, 1, "Badge for Available Updates should now be 1");
+ maybeRunNext();
+ }, false);
+
+ gCategoryUtilities.openType("extension", function() {
+ gProvider.createInstalls([{
+ name: "manually updating addon (new and improved!)",
+ existingAddon: gProvider.addons[1],
+ version: "1.1",
+ releaseNotesURI: Services.io.newURI(TESTROOT + "thereIsNoFileHere.xhtml", null, null)
+ }]);
+
+ var item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+ get_tooltip_info(item).then(({ version }) => {
+ is(version, "1.0", "Should still show the old version in the tooltip");
+ maybeRunNext();
+ });
+ });
+});
+
+
+add_test(function() {
+ wait_for_view_load(gManagerWindow, function() {
+ is(gManagerWindow.document.getElementById("categories").selectedItem.value, "addons://updates/available", "Available Updates category should now be selected");
+ is(gManagerWindow.gViewController.currentViewId, "addons://updates/available", "Available Updates view should be the current view");
+ run_next_test();
+ }, true);
+ EventUtils.synthesizeMouseAtCenter(gAvailableCategory, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("updates-list");
+ is(list.itemCount, 1, "Should be 1 available update listed");
+ var item = list.firstChild;
+ is(item.mAddon.id, "addon2@tests.mozilla.org", "Update item should be for the manually updating addon");
+
+ // The item in the list will be checking for update information asynchronously
+ // so we have to wait for it to complete. Doing the same async request should
+ // make our callback be called later.
+ AddonManager.getAllInstalls(run_next_test);
+});
+
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("updates-list");
+ var item = list.firstChild;
+ get_tooltip_info(item).then(({ version }) => {
+ is(version, "1.1", "Update item should have version number of the update");
+ var postfix = gManagerWindow.document.getAnonymousElementByAttribute(item, "class", "update-postfix");
+ is_element_visible(postfix, "'Update' postfix should be visible");
+ is_element_visible(item._updateAvailable, "");
+ is_element_visible(item._relNotesToggle, "Release notes toggle should be visible");
+ is_element_hidden(item._warning, "Incompatible warning should be hidden");
+ is_element_hidden(item._error, "Blocklist error should be hidden");
+
+ info("Opening release notes");
+ item.addEventListener("RelNotesToggle", function() {
+ item.removeEventListener("RelNotesToggle", arguments.callee, false);
+ info("Release notes now open");
+
+ is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
+ is_element_visible(item._relNotesError, "Release notes error message should be visible");
+ is(item._relNotes.childElementCount, 0, "Release notes should be empty");
+
+ info("Closing release notes");
+ item.addEventListener("RelNotesToggle", function() {
+ item.removeEventListener("RelNotesToggle", arguments.callee, false);
+ info("Release notes now closed");
+ info("Setting Release notes URI to something that should load");
+ gProvider.installs[0].releaseNotesURI = Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
+
+ info("Re-opening release notes");
+ item.addEventListener("RelNotesToggle", function() {
+ item.removeEventListener("RelNotesToggle", arguments.callee, false);
+ info("Release notes now open");
+
+ is_element_hidden(item._relNotesLoading, "Release notes loading message should be hidden");
+ is_element_hidden(item._relNotesError, "Release notes error message should be hidden");
+ isnot(item._relNotes.childElementCount, 0, "Release notes should have been inserted into container");
+ run_next_test();
+
+ }, false);
+ EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
+ is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
+
+ }, false);
+ EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
+
+ }, false);
+ EventUtils.synthesizeMouseAtCenter(item._relNotesToggle, { }, gManagerWindow);
+ is_element_visible(item._relNotesLoading, "Release notes loading message should be visible");
+ });
+});
+
+
+add_test(function() {
+ var badgeUpdated = false;
+ var installCompleted = false;
+
+ gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
+ gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
+ if (installCompleted)
+ run_next_test();
+ else
+ badgeUpdated = true;
+ }, false);
+
+ var list = gManagerWindow.document.getElementById("updates-list");
+ var item = list.firstChild;
+ var updateBtn = item._updateBtn;
+ is_element_visible(updateBtn, "Update button should be visible");
+
+ var install = gProvider.installs[0];
+ var listener = {
+ onInstallStarted: function() {
+ info("Install started");
+ is_element_visible(item._installStatus, "Install progress widget should be visible");
+ },
+ onInstallEnded: function() {
+ install.removeTestListener(this);
+ info("Install ended");
+ is_element_hidden(item._installStatus, "Install progress widget should be hidden");
+
+ if (badgeUpdated)
+ run_next_test();
+ else
+ installCompleted = true;
+ }
+ };
+ install.addTestListener(listener);
+ EventUtils.synthesizeMouseAtCenter(updateBtn, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ is(gCategoryUtilities.isVisible(gAvailableCategory), true, "Available Updates category should still be visible");
+ is(gAvailableCategory.badgeCount, 0, "Badge for Available Updates should now be 0");
+
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should be hidden");
+
+ close_manager(gManagerWindow, function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ gAvailableCategory = gManagerWindow.gCategories.get("addons://updates/available");
+
+ is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should be hidden");
+
+ run_next_test();
+ });
+ });
+ });
+});
+
+add_test(function() {
+ gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
+ gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
+ is(gCategoryUtilities.isVisible(gAvailableCategory), true, "Available Updates category should now be visible");
+ is(gAvailableCategory.badgeCount, 1, "Badge for Available Updates should now be 1");
+
+ gAvailableCategory.addEventListener("CategoryBadgeUpdated", function() {
+ gAvailableCategory.removeEventListener("CategoryBadgeUpdated", arguments.callee, false);
+ is(gCategoryUtilities.isVisible(gAvailableCategory), false, "Available Updates category should now be hidden");
+
+ run_next_test();
+ }, false);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+ aAddon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ });
+ }, false);
+
+ gProvider.createInstalls([{
+ name: "manually updating addon (new and even more improved!)",
+ existingAddon: gProvider.addons[1],
+ version: "1.2",
+ releaseNotesURI: Services.io.newURI(TESTROOT + "thereIsNoFileHere.xhtml", null, null)
+ }]);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_metadataTimeout.js b/toolkit/mozapps/extensions/test/browser/browser_metadataTimeout.js
new file mode 100644
index 000000000..98be4b7f8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_metadataTimeout.js
@@ -0,0 +1,114 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test how update window behaves when metadata ping times out
+// bug 965788
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_MIN_PLATFORM_COMPAT = "extensions.minCompatiblePlatformVersion";
+const PREF_METADATA_LASTUPDATE = "extensions.getAddons.cache.lastUpdate";
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+var repo = {};
+var ARContext = Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm", repo);
+
+// Mock out the XMLHttpRequest factory for AddonRepository so
+// we can reply with a timeout
+var pXHRStarted = Promise.defer();
+var oldXHRConstructor = ARContext.ServiceRequest;
+ARContext.ServiceRequest = function() {
+ this._handlers = new Map();
+ this.mozBackgroundRequest = false;
+ this.timeout = undefined;
+ this.open = function(aMethod, aURI, aAsync) {
+ this.method = aMethod;
+ this.uri = aURI;
+ this.async = aAsync;
+ info("Opened XHR for " + aMethod + " " + aURI);
+ };
+ this.overrideMimeType = function(aMimeType) {
+ this.mimeType = aMimeType;
+ };
+ this.addEventListener = function(aEvent, aHandler, aCapture) {
+ this._handlers.set(aEvent, aHandler);
+ };
+ this.send = function(aBody) {
+ info("Send XHR for " + this.method + " " + this.uri + " handlers: " + [this._handlers.keys()].join(", "));
+ pXHRStarted.resolve(this);
+ }
+};
+
+
+// Returns promise{window}, resolves with a handle to the compatibility
+// check window
+function promise_open_compatibility_window(aInactiveAddonIds) {
+ let deferred = Promise.defer();
+ // This will reset the longer timeout multiplier to 2 which will give each
+ // test that calls open_compatibility_window a minimum of 60 seconds to
+ // complete.
+ requestLongerTimeout(2);
+
+ var variant = Cc["@mozilla.org/variant;1"].
+ createInstance(Ci.nsIWritableVariant);
+ variant.setFromVariant(aInactiveAddonIds);
+
+ // Cannot be modal as we want to interract with it, shouldn't cause problems
+ // with testing though.
+ var features = "chrome,centerscreen,dialog,titlebar";
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ var win = ww.openWindow(null, URI_EXTENSION_UPDATE_DIALOG, "", features, variant);
+
+ win.addEventListener("load", function() {
+ function page_shown(aEvent) {
+ if (aEvent.target.pageid)
+ info("Page " + aEvent.target.pageid + " shown");
+ }
+
+ win.removeEventListener("load", arguments.callee, false);
+
+ info("Compatibility dialog opened");
+
+ win.addEventListener("pageshow", page_shown, false);
+ win.addEventListener("unload", function() {
+ win.removeEventListener("unload", arguments.callee, false);
+ win.removeEventListener("pageshow", page_shown, false);
+ dump("Compatibility dialog closed\n");
+ }, false);
+
+ deferred.resolve(win);
+ }, false);
+ return deferred.promise;
+}
+
+function promise_window_close(aWindow) {
+ let deferred = Promise.defer();
+ aWindow.addEventListener("unload", function() {
+ aWindow.removeEventListener("unload", arguments.callee, false);
+ deferred.resolve(aWindow);
+ }, false);
+ return deferred.promise;
+}
+
+// Start the compatibility update dialog, but use the mock XHR to respond with
+// a timeout
+add_task(function* amo_ping_timeout() {
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ Services.prefs.clearUserPref(PREF_METADATA_LASTUPDATE);
+ let compatWindow = yield promise_open_compatibility_window([]);
+
+ let xhr = yield pXHRStarted.promise;
+ is(xhr.timeout, 30000, "XHR request should have 30 second timeout");
+ ok(xhr._handlers.has("timeout"), "Timeout handler set on XHR");
+ // call back the timeout handler
+ xhr._handlers.get("timeout")();
+
+ // Put the old XHR constructor back
+ ARContext.ServiceRequest = oldXHRConstructor;
+ // The window should close without further interaction
+ yield promise_window_close(compatWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_newaddon.js b/toolkit/mozapps/extensions/test/browser/browser_newaddon.js
new file mode 100644
index 000000000..d450828ba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_newaddon.js
@@ -0,0 +1,232 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the new add-on tab
+
+var gProvider;
+
+function loadPage(aURL, aCallback, aBackground = false) {
+ let tab = gBrowser.addTab();
+ if (!aBackground)
+ gBrowser.selectedTab = tab;
+ let browser = tab.linkedBrowser;
+ browser.loadURI(aURL);
+ browser.addEventListener("AddonDisplayed", function(event) {
+ browser.removeEventListener("AddonDisplayed", arguments.callee, false);
+
+ aCallback(tab);
+ });
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test 1",
+ version: "5.3",
+ userDisabled: true,
+ seen: false,
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test 2",
+ version: "7.1",
+ creator: "Dave Townsend",
+ userDisabled: true,
+ seen: false
+ }]);
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+// Tests that ignoring a restartless add-on works
+add_test(function() {
+ loadPage("about:newaddon?id=addon1@tests.mozilla.org", function(aTab) {
+ var doc = aTab.linkedBrowser.contentDocument;
+ is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
+
+ is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
+ is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ ok(aAddon.seen, "Add-on should have been marked as seen");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ is(gBrowser.tabs.length, 1, "Page should have been closed");
+
+ ok(aAddon.userDisabled, "Add-on should not have been enabled");
+
+ ok(!aAddon.isActive, "Add-on should not be running");
+
+ aAddon.seen = false;
+ run_next_test();
+ });
+ });
+});
+
+// Tests that enabling a restartless add-on works
+add_test(function() {
+ loadPage("about:newaddon?id=addon1@tests.mozilla.org", function(aTab) {
+ var doc = aTab.linkedBrowser.contentDocument;
+ is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
+
+ is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
+ is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ ok(aAddon.seen, "Add-on should have been marked as seen");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ is(gBrowser.tabs.length, 1, "Page should have been closed");
+
+ ok(!aAddon.userDisabled, "Add-on should now have been enabled");
+
+ ok(aAddon.isActive, "Add-on should now be running");
+
+ aAddon.userDisabled = true;
+ aAddon.seen = false;
+ run_next_test();
+ });
+ });
+});
+
+// Tests that ignoring a non-restartless add-on works
+add_test(function() {
+ loadPage("about:newaddon?id=addon2@tests.mozilla.org", function(aTab) {
+ var doc = aTab.linkedBrowser.contentDocument;
+ is(doc.getElementById("name").value, "Test 2 7.1", "Should say the right name");
+
+ is_element_visible(doc.getElementById("author"), "Should be an author displayed");
+ is(doc.getElementById("author").value, "By Dave Townsend", "Should have the right author");
+ is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+ ok(aAddon.seen, "Add-on should have been marked as seen");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ is(gBrowser.tabs.length, 1, "Page should have been closed");
+
+ ok(aAddon.userDisabled, "Add-on should not have been enabled");
+
+ ok(!aAddon.isActive, "Add-on should not be running");
+
+ aAddon.seen = false;
+ run_next_test();
+ });
+ });
+});
+
+// Tests that enabling a non-restartless add-on works
+add_test(function() {
+ loadPage("about:newaddon?id=addon2@tests.mozilla.org", function(aTab) {
+ var doc = aTab.linkedBrowser.contentDocument;
+ is(doc.getElementById("name").value, "Test 2 7.1", "Should say the right name");
+
+ is_element_visible(doc.getElementById("author"), "Should be an author displayed");
+ is(doc.getElementById("author").value, "By Dave Townsend", "Should have the right author");
+ is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(aAddon) {
+ ok(aAddon.seen, "Add-on should have been marked as seen");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("restartPanel"),
+ "Should be showing the right buttons");
+
+ ok(!aAddon.userDisabled, "Add-on should now have been enabled");
+
+ ok(!aAddon.isActive, "Add-on should not be running");
+
+ ok(doc.getElementById("allow").disabled, "Should have disabled checkbox");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("cancel-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ ok(!doc.getElementById("allow").disabled, "Should have enabled checkbox");
+
+ ok(aAddon.userDisabled, "Add-on should not have been enabled");
+
+ ok(!aAddon.isActive, "Add-on should not be running");
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("allow"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ EventUtils.synthesizeMouseAtCenter(doc.getElementById("continue-button"),
+ {}, aTab.linkedBrowser.contentWindow);
+
+ ok(aAddon.userDisabled, "Add-on should not have been enabled");
+
+ ok(!aAddon.isActive, "Add-on should not be running");
+
+ is(gBrowser.tabs.length, 1, "Page should have been closed");
+
+ aAddon.seen = false;
+ run_next_test();
+ });
+ });
+});
+
+// Tests that opening the page in the background doesn't mark as seen
+add_test(function() {
+ loadPage("about:newaddon?id=addon1@tests.mozilla.org", function(aTab) {
+ var doc = aTab.linkedBrowser.contentDocument;
+ is(doc.getElementById("name").value, "Test 1 5.3", "Should say the right name");
+
+ is_element_hidden(doc.getElementById("author"), "Should be no author displayed");
+ is_element_hidden(doc.getElementById("location"), "Should be no location displayed");
+
+ is(doc.getElementById("buttonDeck").selectedPanel, doc.getElementById("continuePanel"),
+ "Should be showing the right buttons");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ ok(!aAddon.seen, "Add-on should not have been marked as seen.");
+
+ gBrowser.selectedTab = aTab;
+
+ waitForFocus(function() {
+ ok(aAddon.seen, "Add-on should have been marked as seen after focusing the tab.");
+
+ gBrowser.removeTab(aTab);
+
+ run_next_test();
+ }, aTab.linkedBrowser.contentWindow);
+ });
+ }, true);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_openDialog.js b/toolkit/mozapps/extensions/test/browser/browser_openDialog.js
new file mode 100644
index 000000000..f95365a4c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_openDialog.js
@@ -0,0 +1,173 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the dialog open by the Options button for addons that provide a
+// custom chrome-like protocol for optionsURL.
+
+var CustomChromeProtocol = {
+ scheme: "khrome",
+ defaultPort: -1,
+ protocolFlags: Ci.nsIProtocolHandler.URI_DANGEROUS_TO_LOAD |
+ Ci.nsIProtocolHandler.URI_IS_LOCAL_RESOURCE |
+ Ci.nsIProtocolHandler.URI_NORELATIVE |
+ Ci.nsIProtocolHandler.URI_NOAUTH,
+
+ newURI: function CCP_newURI(aSpec, aOriginCharset, aBaseUri) {
+ let uri = Cc["@mozilla.org/network/simple-uri;1"].
+ createInstance(Ci.nsIURI);
+ uri.spec = aSpec;
+ return uri;
+ },
+
+ newChannel2: function CCP_newChannel2(aURI, aLoadInfo) {
+ let url = Services.io.newURI("chrome:" + aURI.path, null, null);
+ let ch = Services.io.newChannelFromURIWithLoadInfo(url, aLoadInfo);
+ ch.originalURI = aURI;
+ return ch;
+ },
+
+ newChannel: function CCP_newChannel(aURI) {
+ return this.newChannel2(aURI, null);
+ },
+
+ allowPort: function CCP_allowPort(aPort, aScheme) {
+ return false;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIProtocolHandler
+ ]),
+
+ classID: Components.ID("{399cb2d1-05dd-4363-896f-63b78e008cf8}"),
+
+ factory: {
+ registrar: Components.manager.QueryInterface(Ci.nsIComponentRegistrar),
+
+ register: function CCP_register() {
+ this.registrar.registerFactory(
+ CustomChromeProtocol.classID,
+ "CustomChromeProtocol",
+ "@mozilla.org/network/protocol;1?name=khrome",
+ this
+ );
+ },
+
+ unregister: function CCP_register() {
+ this.registrar.unregisterFactory(CustomChromeProtocol.classID, this);
+ },
+
+ // nsIFactory
+ createInstance: function BNPH_createInstance(aOuter, aIID) {
+ if (aOuter) {
+ throw Components.Exception("Class does not allow aggregation",
+ Components.results.NS_ERROR_NO_AGGREGATION);
+ }
+ return CustomChromeProtocol.QueryInterface(aIID);
+ },
+
+ lockFactory: function BNPH_lockFactory(aLock) {
+ throw Components.Exception("Function lockFactory is not implemented",
+ Components.results.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([
+ Ci.nsIFactory
+ ])
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ requestLongerTimeout(2);
+
+ info("Registering custom chrome-like protocol.");
+ CustomChromeProtocol.factory.register();
+ registerCleanupFunction(() => CustomChromeProtocol.factory.unregister());
+
+ const ADDONS_LIST = [
+ { id: "test1@tests.mozilla.org",
+ name: "Test add-on 1",
+ optionsURL: CHROMEROOT + "addon_prefs.xul" },
+ { id: "test2@tests.mozilla.org",
+ name: "Test add-on 2",
+ optionsURL: (CHROMEROOT + "addon_prefs.xul").replace("chrome:", "khrome:") },
+ ];
+
+ var gProvider = new MockProvider();
+ gProvider.createAddons(ADDONS_LIST);
+
+ open_manager("addons://list/extension", function(aManager) {
+ let addonList = aManager.document.getElementById("addon-list");
+ let currentAddon;
+ let instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply");
+
+ function getAddonByName(aName) {
+ for (let addonItem of addonList.childNodes) {
+ if (addonItem.hasAttribute("name") &&
+ addonItem.getAttribute("name") == aName)
+ return addonItem;
+ }
+ return null;
+ }
+
+ function observer(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "domwindowclosed":
+ // Give the preference window a chance to finish closing before
+ // closing the add-ons manager.
+ waitForFocus(function () {
+ test_next_addon();
+ });
+ break;
+ case "domwindowopened":
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ waitForFocus(function () {
+ // If the openDialog privileges are wrong a new browser window
+ // will open, let the test proceed (and fail) rather than timeout.
+ if (win.location != currentAddon.optionsURL &&
+ win.location != "chrome://browser/content/browser.xul")
+ return;
+
+ is(win.location, currentAddon.optionsURL,
+ "The correct addon pref window should have opened");
+
+ let chromeFlags = win.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIXULWindow).chromeFlags;
+ ok(chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME &&
+ (instantApply || chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG),
+ "Window was open as a chrome dialog.");
+
+ win.close();
+ }, win);
+ break;
+ }
+ }
+
+ function test_next_addon() {
+ currentAddon = ADDONS_LIST.shift();
+ if (!currentAddon) {
+ Services.ww.unregisterNotification(observer);
+ close_manager(aManager, finish);
+ return;
+ }
+
+ info("Testing " + currentAddon.name);
+ let addonItem = getAddonByName(currentAddon.name, addonList);
+ let optionsBtn =
+ aManager.document.getAnonymousElementByAttribute(addonItem, "anonid",
+ "preferences-btn");
+ is(optionsBtn.hidden, false, "Prefs button should be visible.")
+
+ addonList.ensureElementIsVisible(addonItem);
+ EventUtils.synthesizeMouseAtCenter(optionsBtn, { }, aManager);
+ }
+
+ Services.ww.registerNotification(observer);
+ test_next_addon();
+ });
+
+}
diff --git a/toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js b/toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js
new file mode 100644
index 000000000..a9c7be4bc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_plugin_enabled_state_locked.js
@@ -0,0 +1,124 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that state menu is displayed correctly (enabled or disabled) in the add-on manager
+// when the preference is unlocked / locked
+var {classes: Cc, interfaces: Ci} = Components;
+const gIsWindows = ("@mozilla.org/windows-registry-key;1" in Cc);
+const gIsOSX = ("nsILocalFileMac" in Ci);
+const gIsLinux = ("@mozilla.org/gnome-gconf-service;1" in Cc) ||
+ ("@mozilla.org/gio-service;1" in Cc);
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gPluginElement;
+
+function getTestPluginPref() {
+ let prefix = "plugin.state.";
+ if (gIsWindows)
+ return `${prefix}nptest`;
+ if (gIsLinux)
+ return `${prefix}libnptest`;
+ return `${prefix}test`;
+}
+
+registerCleanupFunction(() => {
+ Services.prefs.unlockPref(getTestPluginPref());
+ Services.prefs.clearUserPref(getTestPluginPref());
+});
+
+function getPlugins() {
+ return new Promise(resolve => {
+ AddonManager.getAddonsByTypes(["plugin"], plugins => resolve(plugins));
+ });
+}
+
+function getTestPlugin(aPlugins) {
+ let testPluginId;
+
+ for (let plugin of aPlugins) {
+ if (plugin.name == "Test Plug-in") {
+ testPluginId = plugin.id;
+ break;
+ }
+ }
+
+ Assert.ok(testPluginId, "Test Plug-in should exist");
+
+ let pluginElement = get_addon_element(gManagerWindow, testPluginId);
+ pluginElement.parentNode.ensureElementIsVisible(pluginElement);
+
+ return pluginElement;
+}
+
+function checkStateMenu(locked) {
+ Assert.equal(Services.prefs.prefIsLocked(getTestPluginPref()), locked,
+ "Preference lock state should be correct.");
+ let menuList = gManagerWindow.document.getAnonymousElementByAttribute(gPluginElement, "anonid", "state-menulist");
+ // State menu should always have a selected item which must be visible
+ let selectedMenuItem = menuList.querySelector(".addon-control[selected=\"true\"]");
+
+ is_element_visible(menuList, "State menu should be visible.");
+ Assert.equal(menuList.disabled, locked,
+ "State menu should" + (locked === true ? "" : " not") + " be disabled.");
+
+ is_element_visible(selectedMenuItem, "State menu's selected item should be visible.");
+}
+
+function checkStateMenuDetail(locked) {
+ Assert.equal(Services.prefs.prefIsLocked(getTestPluginPref()), locked,
+ "Preference should be " + (locked === true ? "" : "un") + "locked.");
+
+ // open details menu
+ let details = gManagerWindow.document.getAnonymousElementByAttribute(gPluginElement, "anonid", "details-btn");
+ is_element_visible(details, "Details link should be visible.");
+ EventUtils.synthesizeMouseAtCenter(details, {}, gManagerWindow);
+
+ return new Promise(resolve => {
+ wait_for_view_load(gManagerWindow, function() {
+ let menuList = gManagerWindow.document.getElementById("detail-state-menulist");
+ is_element_visible(menuList, "Details state menu should be visible.");
+ Assert.equal(menuList.disabled, locked,
+ "Details state menu enabled state should be correct.");
+ resolve();
+ });
+ });
+}
+
+add_task(function* initializeState() {
+ Services.prefs.setIntPref(getTestPluginPref(), Ci.nsIPluginTag.STATE_ENABLED);
+ Services.prefs.unlockPref(getTestPluginPref());
+ gManagerWindow = yield open_manager();
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ yield gCategoryUtilities.openType("plugin");
+
+ let plugins = yield getPlugins();
+ gPluginElement = getTestPlugin(plugins);
+});
+
+// Tests that plugin state menu is enabled if the preference is unlocked
+add_task(function* taskCheckStateMenuIsEnabled() {
+ checkStateMenu(false);
+ yield checkStateMenuDetail(false);
+});
+
+// Lock the preference and then reload the plugin category
+add_task(function* reinitializeState() {
+ // lock the preference
+ Services.prefs.lockPref(getTestPluginPref());
+ yield gCategoryUtilities.openType("plugin");
+ // Retrieve the test plugin element
+ let plugins = yield getPlugins();
+ gPluginElement = getTestPlugin(plugins);
+});
+
+// Tests that plugin state menu is disabled if the preference is locked
+add_task(function* taskCheckStateMenuIsDisabled() {
+ checkStateMenu(true);
+ yield checkStateMenuDetail(true);
+});
+
+add_task(function* testCleanup() {
+ yield close_manager(gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_pluginprefs.js b/toolkit/mozapps/extensions/test/browser/browser_pluginprefs.js
new file mode 100644
index 000000000..458e8e334
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_pluginprefs.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the detail view of plugins
+
+var gManagerWindow;
+
+function test() {
+ waitForExplicitFinish();
+
+ open_manager("addons://list/plugin", function(aWindow) {
+ gManagerWindow = aWindow;
+
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+add_test(function() {
+ AddonManager.getAddonsByTypes(["plugin"], function(plugins) {
+ let testPluginId;
+ for (let plugin of plugins) {
+ if (plugin.name == "Test Plug-in") {
+ testPluginId = plugin.id;
+ break;
+ }
+ }
+ ok(testPluginId, "Test Plug-in should exist")
+
+ AddonManager.getAddonByID(testPluginId, function(testPlugin) {
+ let pluginEl = get_addon_element(gManagerWindow, testPluginId);
+ is(pluginEl.mAddon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_INFO, "Options should be inline info type");
+ pluginEl.parentNode.ensureElementIsVisible(pluginEl);
+
+ let button = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "preferences-btn");
+ is_element_hidden(button, "Preferences button should be hidden");
+
+ button = gManagerWindow.document.getAnonymousElementByAttribute(pluginEl, "anonid", "details-btn");
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ let pluginLibraries = gManagerWindow.document.getElementById("pluginLibraries");
+ ok(pluginLibraries, "Plugin file name row should be displayed");
+ // the file name depends on the platform
+ ok(pluginLibraries.textContent, testPlugin.pluginLibraries, "Plugin file name should be displayed");
+
+ let pluginMimeTypes = gManagerWindow.document.getElementById("pluginMimeTypes");
+ ok(pluginMimeTypes, "Plugin mime type row should be displayed");
+ ok(pluginMimeTypes.textContent, "application/x-test (tst)", "Plugin mime type should be displayed");
+
+ run_next_test();
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_purchase.js b/toolkit/mozapps/extensions/test/browser/browser_purchase.js
new file mode 100644
index 000000000..a815d10d5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_purchase.js
@@ -0,0 +1,197 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that marketplace results show up in searches, are sorted right and
+// attempting to buy links through to the right webpage
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_purchase.xml";
+
+var gManagerWindow;
+
+function test() {
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, SEARCH_URL);
+
+ waitForExplicitFinish();
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+
+ waitForFocus(function() {
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "foo";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var remoteFilter = gManagerWindow.document.getElementById("search-filter-remote");
+ EventUtils.synthesizeMouseAtCenter(remoteFilter, { }, gManagerWindow);
+
+ run_next_test();
+ });
+ }, aWindow);
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ // Will have created an install so cancel it
+ AddonManager.getAllInstalls(function(aInstalls) {
+ is(aInstalls.length, 1, "Should have been one install created");
+ aInstalls[0].cancel();
+
+ finish();
+ });
+ });
+}
+
+function get_node(parent, anonid) {
+ return parent.ownerDocument.getAnonymousElementByAttribute(parent, "anonid", anonid);
+}
+
+function get_install_btn(parent) {
+ var installStatus = get_node(parent, "install-status");
+ return get_node(installStatus, "install-remote-btn");
+}
+
+function get_purchase_btn(parent) {
+ var installStatus = get_node(parent, "install-status");
+ return get_node(installStatus, "purchase-remote-btn");
+}
+
+// Tests that the expected results appeared
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("search-list");
+ var items = Array.filter(list.childNodes, function(e) {
+ return e.tagName == "richlistitem";
+ });
+
+ is(items.length, 5, "Should be 5 results");
+
+ is(get_node(items[0], "name").value, "Ludicrously Expensive Add-on", "Add-on 0 should be in expected position");
+ is_element_hidden(get_install_btn(items[0]), "Add-on 0 install button should be hidden");
+ is_element_visible(get_purchase_btn(items[0]), "Add-on 0 purchase button should be visible");
+ is(get_purchase_btn(items[0]).label, "Purchase for $101\u2026", "Add-on 0 should have the right price");
+
+ is(get_node(items[1], "name").value, "Cheap Add-on", "Add-on 1 should be in expected position");
+ is_element_hidden(get_install_btn(items[1]), "Add-on 1 install button should be hidden");
+ is_element_visible(get_purchase_btn(items[1]), "Add-on 1 purchase button should be visible");
+ is(get_purchase_btn(items[1]).label, "Purchase for $0.99\u2026", "Add-on 2 should have the right price");
+
+ is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+ is_element_hidden(get_install_btn(items[2]), "Add-on 2 install button should be hidden");
+ is_element_visible(get_purchase_btn(items[2]), "Add-on 2 purchase button should be visible");
+ is(get_purchase_btn(items[2]).label, "Purchase for $1\u2026", "Add-on 3 should have the right price");
+
+ is(get_node(items[3], "name").value, "Free Add-on", "Add-on 3 should be in expected position");
+ is_element_visible(get_install_btn(items[3]), "Add-on 3 install button should be visible");
+ is_element_hidden(get_purchase_btn(items[3]), "Add-on 3 purchase button should be hidden");
+
+ is(get_node(items[4], "name").value, "More Expensive Add-on", "Add-on 4 should be in expected position");
+ is_element_hidden(get_install_btn(items[4]), "Add-on 4 install button should be hidden");
+ is_element_visible(get_purchase_btn(items[4]), "Add-on 4 purchase button should be visible");
+ is(get_purchase_btn(items[4]).label, "Purchase for $1.01\u2026", "Add-on 4 should have the right price");
+
+ run_next_test();
+});
+
+// Tests that sorting by price works
+add_test(function() {
+ var list = gManagerWindow.document.getElementById("search-list");
+
+ var sorters = gManagerWindow.document.getElementById("search-sorters");
+ var priceSorter = get_node(sorters, "price-btn");
+ info("Changing sort order");
+ EventUtils.synthesizeMouseAtCenter(priceSorter, { }, gManagerWindow);
+
+ var items = Array.filter(list.childNodes, function(e) {
+ return e.tagName == "richlistitem";
+ });
+
+ is(get_node(items[0], "name").value, "Free Add-on", "Add-on 0 should be in expected position");
+ is(get_node(items[1], "name").value, "Cheap Add-on", "Add-on 1 should be in expected position");
+ is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+ is(get_node(items[3], "name").value, "More Expensive Add-on", "Add-on 3 should be in expected position");
+ is(get_node(items[4], "name").value, "Ludicrously Expensive Add-on", "Add-on 4 should be in expected position");
+
+ info("Changing sort order");
+ EventUtils.synthesizeMouseAtCenter(priceSorter, { }, gManagerWindow);
+
+ items = Array.filter(list.childNodes, function(e) {
+ return e.tagName == "richlistitem";
+ });
+
+ is(get_node(items[0], "name").value, "Ludicrously Expensive Add-on", "Add-on 0 should be in expected position");
+ is(get_node(items[1], "name").value, "More Expensive Add-on", "Add-on 1 should be in expected position");
+ is(get_node(items[2], "name").value, "Reasonable Add-on", "Add-on 2 should be in expected position");
+ is(get_node(items[3], "name").value, "Cheap Add-on", "Add-on 3 should be in expected position");
+ is(get_node(items[4], "name").value, "Free Add-on", "Add-on 4 should be in expected position");
+
+ run_next_test();
+});
+
+// Tests that clicking the buy button works from the list
+add_test(function() {
+ gBrowser.tabContainer.addEventListener("TabOpen", function listener(event) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", listener, true);
+ function wantLoad(url) {
+ return url != "about:blank";
+ }
+ BrowserTestUtils.browserLoaded(event.target.linkedBrowser, false, wantLoad).then(() => {
+ is(gBrowser.currentURI.spec, TESTROOT + "releaseNotes.xhtml?addon5", "Should have loaded the right page");
+
+ gBrowser.removeCurrentTab();
+
+ if (gUseInContentUI) {
+ is(gBrowser.currentURI.spec, "about:addons", "Should be back to the add-ons manager");
+ run_next_test();
+ }
+ else {
+ waitForFocus(run_next_test, gManagerWindow);
+ }
+ });
+ }, true);
+
+ var list = gManagerWindow.document.getElementById("search-list");
+ EventUtils.synthesizeMouseAtCenter(get_purchase_btn(list.firstChild), { }, gManagerWindow);
+});
+
+// Tests that clicking the buy button from the details view works
+add_test(function() {
+ gBrowser.tabContainer.addEventListener("TabOpen", function listener(event) {
+ gBrowser.tabContainer.removeEventListener("TabOpen", listener, true);
+ function wantLoad(url) {
+ return url != "about:blank";
+ }
+ BrowserTestUtils.browserLoaded(event.target.linkedBrowser, false, wantLoad).then(() => {
+ is(gBrowser.currentURI.spec, TESTROOT + "releaseNotes.xhtml?addon4", "Should have loaded the right page");
+
+ gBrowser.removeCurrentTab();
+
+ if (gUseInContentUI) {
+ is(gBrowser.currentURI.spec, "about:addons", "Should be back to the add-ons manager");
+ run_next_test();
+ }
+ else {
+ waitForFocus(run_next_test, gManagerWindow);
+ }
+ });
+ }, true);
+
+ var list = gManagerWindow.document.getElementById("search-list");
+ var item = list.firstChild.nextSibling;
+ list.ensureElementIsVisible(item);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ var btn = gManagerWindow.document.getElementById("detail-purchase-btn");
+ is_element_visible(btn, "Purchase button should be visible");
+
+ EventUtils.synthesizeMouseAtCenter(btn, { }, gManagerWindow);
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_purchase.xml b/toolkit/mozapps/extensions/test/browser/browser_purchase.xml
new file mode 100644
index 000000000..9d4b18880
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_purchase.xml
@@ -0,0 +1,180 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>Ludicrously Expensive Add-on</name>
+ <type id='1'>Extension</type>
+ <guid>addon5@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <payment_data>
+ <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon5</link>
+ <amount amount="101">$101</amount>
+ </payment_data>
+ </addon>
+ <addon>
+ <name>Cheap Add-on</name>
+ <type id='1'>Extension</type>
+ <guid>addon2@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <payment_data>
+ <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon2</link>
+ <amount amount="0.99">$0.99</amount>
+ </payment_data>
+ </addon>
+ <addon>
+ <name>Reasonable Add-on</name>
+ <type id='1'>Extension</type>
+ <guid>addon3@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <payment_data>
+ <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon3</link>
+ <amount amount="1">$1</amount>
+ </payment_data>
+ </addon>
+ <addon>
+ <name>Free Add-on</name>
+ <type id='1'>Extension</type>
+ <guid>addon1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <install size="1">http://example.com/addon1.xpi</install>
+ </addon>
+ <addon>
+ <name>More Expensive Add-on</name>
+ <type id='1'>Extension</type>
+ <guid>addon4@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <payment_data>
+ <link>http://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml?addon4</link>
+ <amount amount="1.01">$1.01</amount>
+ </payment_data>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_recentupdates.js b/toolkit/mozapps/extensions/test/browser/browser_recentupdates.js
new file mode 100644
index 000000000..02007dbbf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_recentupdates.js
@@ -0,0 +1,125 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the recent updates pane
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "updated 6 hours ago",
+ version: "1.0",
+ updateDate: new Date(Date.now() - (1000 * 60 * 60 * 6)),
+ releaseNotesURI: Services.io.newURI(TESTROOT + "releaseNotes.xhtml", null, null)
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "updated 5 seconds ago",
+ version: "1.0",
+ updateDate: new Date(Date.now() - (1000 * 5))
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "updated 1 month ago",
+ version: "1.0",
+ updateDate: new Date(Date.now() - (1000 * 60 * 60 * 25 * 30))
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+
+add_test(function() {
+ info("Checking menuitem for Recent Updates opens that pane");
+ var recentCat = gManagerWindow.gCategories.get("addons://updates/recent");
+ is(gCategoryUtilities.isVisible(recentCat), false, "Recent Updates category should initially be hidden");
+
+ var utilsBtn = gManagerWindow.document.getElementById("header-utils-btn");
+ utilsBtn.addEventListener("popupshown", function() {
+ utilsBtn.removeEventListener("popupshown", arguments.callee, false);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.isVisible(recentCat), true, "Recent Updates category should now be visible");
+ is(gManagerWindow.document.getElementById("categories").selectedItem.value, "addons://updates/recent", "Recent Updates category should now be selected");
+ is(gManagerWindow.gViewController.currentViewId, "addons://updates/recent", "Recent Updates view should be the current view");
+ run_next_test();
+ }, true);
+ var menuitem = gManagerWindow.document.getElementById("utils-viewUpdates");
+ EventUtils.synthesizeMouse(menuitem, 2, 2, { }, gManagerWindow);
+ }, false);
+ EventUtils.synthesizeMouse(utilsBtn, 2, 2, { }, gManagerWindow);
+});
+
+
+add_test(function() {
+ var updatesList = gManagerWindow.document.getElementById("updates-list");
+ var sorters = gManagerWindow.document.getElementById("updates-sorters");
+ var dateSorter = gManagerWindow.document.getAnonymousElementByAttribute(sorters, "anonid", "date-btn");
+ var nameSorter = gManagerWindow.document.getAnonymousElementByAttribute(sorters, "anonid", "name-btn");
+
+ function check_order(expected) {
+ var items = updatesList.getElementsByTagName("richlistitem");
+ var possible = ["addon1@tests.mozilla.org", "addon2@tests.mozilla.org", "addon3@tests.mozilla.org"];
+ for (let item of items) {
+ let itemId = item.mAddon.id;
+ if (possible.indexOf(itemId) == -1)
+ continue; // skip over any other addons, such as shipped addons that would update on every build
+ isnot(expected.length, 0, "Should be expecting more items");
+ is(itemId, expected.shift(), "Should get expected item based on sort order");
+ if (itemId == "addon1@tests.mozilla.org")
+ is_element_visible(item._relNotesToggle, "Release notes toggle should be visible for addon with release notes");
+ else
+ is_element_hidden(item._relNotesToggle, "Release notes toggle should be hidden for addon with no release notes");
+ }
+ }
+
+ is_element_visible(dateSorter);
+ is_element_visible(nameSorter);
+
+ // sorted by date, descending
+ check_order(["addon2@tests.mozilla.org", "addon1@tests.mozilla.org"]);
+
+ // sorted by date, ascending
+ EventUtils.synthesizeMouseAtCenter(dateSorter, { }, gManagerWindow);
+ check_order(["addon1@tests.mozilla.org", "addon2@tests.mozilla.org"]);
+
+ // sorted by name, ascending
+ EventUtils.synthesizeMouseAtCenter(nameSorter, { }, gManagerWindow);
+ check_order(["addon2@tests.mozilla.org", "addon1@tests.mozilla.org"]);
+
+ // sorted by name, descending
+ EventUtils.synthesizeMouseAtCenter(nameSorter, { }, gManagerWindow);
+ check_order(["addon1@tests.mozilla.org", "addon2@tests.mozilla.org"]);
+
+ run_next_test();
+});
+
+
+add_test(function() {
+ close_manager(gManagerWindow, function() {
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ var recentCat = gManagerWindow.gCategories.get("addons://updates/recent");
+ is(gCategoryUtilities.isVisible(recentCat), true, "Recent Updates category should still be visible");
+
+ run_next_test();
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_searching.js b/toolkit/mozapps/extensions/test/browser/browser_searching.js
new file mode 100644
index 000000000..907d9b105
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_searching.js
@@ -0,0 +1,698 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that searching for add-ons works correctly
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+const SEARCH_URL = TESTROOT + "browser_searching.xml";
+const NO_MATCH_URL = TESTROOT + "browser_searching_empty.xml";
+
+const QUERY = "SEARCH";
+const NO_MATCH_QUERY = "NOMATCHQUERY";
+const REMOTE_TO_INSTALL = "remote1";
+const REMOTE_INSTALL_URL = TESTROOT + "addons/browser_searching.xpi";
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+var gServer;
+var gAddonInstalled = false;
+
+function test() {
+ requestLongerTimeout(2);
+ // Turn on searching for this test
+ Services.prefs.setIntPref(PREF_SEARCH_MAXRESULTS, 15);
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "PASS - f",
+ description: "Test description - SEARCH",
+ size: 3,
+ version: "1.0",
+ updateDate: new Date(2010, 4, 2, 0, 0, 1)
+ }, {
+ id: "fail-addon1@tests.mozilla.org",
+ name: "FAIL",
+ description: "Does not match query"
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "PASS - c",
+ description: "Test description - reSEARCHing SEARCH SEARCH",
+ size: 6,
+ version: "2.0",
+ updateDate: new Date(2010, 4, 2, 0, 0, 0)
+ }]);
+
+ var installs = gProvider.createInstalls([{
+ name: "PASS - a - SEARCHing",
+ sourceURI: "http://example.com/install1.xpi"
+ }, {
+ name: "PASS - g - reSEARCHing SEARCH",
+ sourceURI: "http://example.com/install2.xpi"
+ }, {
+ // Does not match query
+ name: "FAIL",
+ sourceURI: "http://example.com/fail-install1.xpi"
+ }]);
+
+ for (let install of installs )
+ install.install();
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ var installedAddon = get_addon_item(REMOTE_TO_INSTALL).mAddon;
+ installedAddon.uninstall();
+
+ AddonManager.getAllInstalls(function(aInstallsList) {
+ for (var install of aInstallsList) {
+ var sourceURI = install.sourceURI.spec;
+ if (sourceURI == REMOTE_INSTALL_URL ||
+ sourceURI.match(/^http:\/\/example\.com\/(.+)\.xpi$/) != null)
+ install.cancel();
+ }
+
+ finish();
+ });
+ });
+}
+
+function getAnonymousElementByAttribute(aElement, aName, aValue) {
+ return gManagerWindow.document.getAnonymousElementByAttribute(aElement,
+ aName,
+ aValue);
+}
+
+/*
+ * Checks whether or not the Add-ons Manager is currently searching
+ *
+ * @param aExpectedSearching
+ * The expected isSearching state
+ */
+function check_is_searching(aExpectedSearching) {
+ var loading = gManagerWindow.document.getElementById("search-loading");
+ is(!is_hidden(loading), aExpectedSearching,
+ "Search throbber should be showing iff currently searching");
+}
+
+/*
+ * Completes a search
+ *
+ * @param aQuery
+ * The query to search for
+ * @param aFinishImmediately
+ * Boolean representing whether or not the search is expected to
+ * finish immediately
+ * @param aCallback
+ * The callback to call when the search is done
+ * @param aCategoryType
+ * The expected selected category after the search is done.
+ * Optional and defaults to "search"
+ */
+function search(aQuery, aFinishImmediately, aCallback, aCategoryType) {
+ // Point search to the correct xml test file
+ var url = (aQuery == NO_MATCH_QUERY) ? NO_MATCH_URL : SEARCH_URL;
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS, url);
+
+ aCategoryType = aCategoryType ? aCategoryType : "search";
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = aQuery;
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ var finishImmediately = true;
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, aCategoryType, "Expected category view should be selected");
+ is(gCategoryUtilities.isTypeVisible("search"), aCategoryType == "search",
+ "Search category should only be visible if it is the current view");
+ check_is_searching(false);
+ is(finishImmediately, aFinishImmediately, "Search should finish immediately only if expected");
+
+ aCallback();
+ });
+
+ finishImmediately = false
+ if (!aFinishImmediately)
+ check_is_searching(true);
+}
+
+/*
+ * Return results of a search
+ *
+ * @return Array of objects, each containing the name and item of a specific
+ * result
+ */
+function get_actual_results() {
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+
+ var results = [];
+ for (var item of rows) {
+
+ // Only consider items that are currently showing
+ var style = gManagerWindow.document.defaultView.getComputedStyle(item, "");
+ if (style.display == "none" || style.visibility != "visible")
+ continue;
+
+ if (item.mInstall || item.isPending("install")) {
+ var sourceURI = item.mInstall.sourceURI.spec;
+ if (sourceURI == REMOTE_INSTALL_URL) {
+ results.push({name: REMOTE_TO_INSTALL, item: item});
+ continue;
+ }
+
+ let result = sourceURI.match(/^http:\/\/example\.com\/(.+)\.xpi$/);
+ if (result != null) {
+ is(item.mInstall.name.indexOf("PASS"), 0, "Install name should start with PASS");
+ results.push({name: result[1], item: item});
+ continue;
+ }
+ }
+ else if (item.mAddon) {
+ let result = item.mAddon.id.match(/^(.+)@tests\.mozilla\.org$/);
+ if (result != null) {
+ is(item.mAddon.name.indexOf("PASS"), 0, "Addon name should start with PASS");
+ results.push({name: result[1], item: item});
+ continue;
+ }
+ }
+ else {
+ ok(false, "Found an item in the list that was neither installing or installed");
+ }
+ }
+
+ return results;
+}
+
+/*
+ * Returns expected results when searching for QUERY with default ordering
+ *
+ * @param aSortBy
+ * How the results are sorted (e.g. "name")
+ * @param aLocalExpected
+ * Boolean representing if local results are expected
+ * @return A pair: [array of results with an expected order,
+ * array of results with unknown order]
+ */
+function get_expected_results(aSortBy, aLocalExpected) {
+ var expectedOrder = null, unknownOrder = null;
+ switch (aSortBy) {
+ case "relevancescore":
+ expectedOrder = [ "addon2", "remote1", "install2", "addon1",
+ "install1", "remote2", "remote3", "remote4" ];
+ unknownOrder = [];
+ break;
+ case "name":
+ // Defaults to ascending order
+ expectedOrder = [ "install1", "remote1", "addon2", "remote2",
+ "remote3", "addon1", "install2", "remote4" ];
+ unknownOrder = [];
+ break;
+ case "dateUpdated":
+ expectedOrder = [ "addon1", "addon2" ];
+ // Updated date not available for installs and remote add-ons
+ unknownOrder = [ "install1", "install2", "remote1",
+ "remote2", "remote3", "remote4" ];
+ break;
+ default:
+ ok(false, "Should recognize sortBy when checking the order of items");
+ }
+
+ // Only keep expected results
+ function filterResults(aId) {
+ // Include REMOTE_TO_INSTALL as a local add-on if it has been installed
+ if (gAddonInstalled && aId == REMOTE_TO_INSTALL)
+ return aLocalExpected;
+
+ if (aId.indexOf("addon") == 0 || aId.indexOf("install") == 0)
+ return aLocalExpected;
+ if (aId.indexOf("remote") == 0)
+ return !aLocalExpected;
+
+ return false;
+ }
+
+
+ return [expectedOrder.filter(filterResults),
+ unknownOrder.filter(filterResults)]
+}
+
+/*
+ * Check that the actual and expected results are the same
+ *
+ * @param aQuery
+ * The search query used
+ * @param aSortBy
+ * How the results are sorted (e.g. "name")
+ * @param aReverseOrder
+ * Boolean representing if the results are in reverse default order
+ * @param aShowLocal
+ * Boolean representing if local results are being shown
+ */
+function check_results(aQuery, aSortBy, aReverseOrder, aShowLocal) {
+
+ var xpinstall_enabled = true;
+ try {
+ xpinstall_enabled = Services.prefs.getBoolPref(PREF_XPI_ENABLED);
+ }
+ catch (e) {}
+
+ // When XPI Instalation is disabled, those buttons are hidden and unused
+ if (xpinstall_enabled) {
+ var localFilterSelected = gManagerWindow.document.getElementById("search-filter-local").selected;
+ var remoteFilterSelected = gManagerWindow.document.getElementById("search-filter-remote").selected;
+ is(localFilterSelected, aShowLocal, "Local filter should be selected if showing local items");
+ is(remoteFilterSelected, !aShowLocal, "Remote filter should be selected if showing remote items");
+ }
+
+ // Get expected order assuming default order
+ var expectedOrder = [], unknownOrder = [];
+ if (aQuery == QUERY)
+ [expectedOrder, unknownOrder] = get_expected_results(aSortBy, aShowLocal);
+
+ // Get actual order of results
+ var actualResults = get_actual_results();
+ var actualOrder = actualResults.map(result => result.name);
+
+ // Reverse array of actual results if supposed to be in reverse order.
+ // Reverse actualOrder instead of expectedOrder so can always check
+ // expectedOrder before unknownOrder
+ if (aReverseOrder)
+ actualOrder.reverse();
+
+ // Check actual vs. expected list of results
+ var totalExpectedResults = expectedOrder.length + unknownOrder.length;
+ is(actualOrder.length, totalExpectedResults, "Should get correct number of results");
+
+ // Check the "first" and "last" attributes are set correctly
+ for (let i = 0; i < actualResults.length; i++) {
+ if (i == 0) {
+ is(actualResults[0].item.hasAttribute("first"), true,
+ "First item should have 'first' attribute set");
+ is(actualResults[0].item.hasAttribute("last"), false,
+ "First item should not have 'last' attribute set");
+ } else if (i == (actualResults.length - 1)) {
+ is(actualResults[actualResults.length - 1].item.hasAttribute("first"), false,
+ "Last item should not have 'first' attribute set");
+ is(actualResults[actualResults.length - 1].item.hasAttribute("last"), true,
+ "Last item should have 'last' attribute set");
+ } else {
+ is(actualResults[i].item.hasAttribute("first"), false,
+ "Item " + i + " should not have 'first' attribute set");
+ is(actualResults[i].item.hasAttribute("last"), false,
+ "Item " + i + " should not have 'last' attribute set");
+ }
+ }
+
+ var i = 0;
+ for (; i < expectedOrder.length; i++)
+ is(actualOrder[i], expectedOrder[i], "Should have seen expected item");
+
+ // Items with data that is unknown can appear in any order among themselves,
+ // so just check that these items exist
+ for (; i < actualOrder.length; i++) {
+ var unknownOrderIndex = unknownOrder.indexOf(actualOrder[i]);
+ ok(unknownOrderIndex >= 0, "Should expect to see item with data that is unknown");
+ unknownOrder[unknownOrderIndex] = null;
+ }
+
+ // Check status of empty notice
+ var emptyNotice = gManagerWindow.document.getElementById("search-list-empty");
+ is(emptyNotice.hidden, totalExpectedResults > 0,
+ "Empty notice should be hidden only if expecting shown items");
+}
+
+/*
+ * Check results of a search with different filterings
+ *
+ * @param aQuery
+ * The search query used
+ * @param aSortBy
+ * How the results are sorted (e.g. "name")
+ * @param aReverseOrder
+ * Boolean representing if the results are in reverse default order
+ * @param aLocalOnly
+ * Boolean representing if the results are local only, can be undefined
+ */
+function check_filtered_results(aQuery, aSortBy, aReverseOrder, aLocalOnly) {
+ var localFilter = gManagerWindow.document.getElementById("search-filter-local");
+ var remoteFilter = gManagerWindow.document.getElementById("search-filter-remote");
+
+ var list = gManagerWindow.document.getElementById("search-list");
+ list.ensureElementIsVisible(localFilter);
+
+ // Check with showing local add-ons
+ EventUtils.synthesizeMouseAtCenter(localFilter, { }, gManagerWindow);
+ check_results(aQuery, aSortBy, aReverseOrder, true);
+
+ // Check with showing remote add-ons
+ aLocalOnly = aLocalOnly || false;
+ EventUtils.synthesizeMouseAtCenter(remoteFilter, { }, gManagerWindow);
+ check_results(aQuery, aSortBy, aReverseOrder, aLocalOnly);
+}
+
+/*
+ * Get item for a specific add-on by name
+ *
+ * @param aName
+ * The name of the add-on to search for
+ * @return Row of add-on if found, null otherwise
+ */
+function get_addon_item(aName) {
+ var id = aName + "@tests.mozilla.org";
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+ for (var row of rows) {
+ if (row.mAddon && row.mAddon.id == id)
+ return row;
+ }
+
+ return null;
+}
+
+/*
+ * Get item for a specific install by name
+ *
+ * @param aName
+ * The name of the install to search for
+ * @return Row of install if found, null otherwise
+ */
+function get_install_item(aName) {
+ var sourceURI = "http://example.com/" + aName + ".xpi";
+ var list = gManagerWindow.document.getElementById("search-list");
+ var rows = list.getElementsByTagName("richlistitem");
+ for (var row of rows) {
+ if (row.mInstall && row.mInstall.sourceURI.spec == sourceURI)
+ return row;
+ }
+
+ return null;
+}
+
+/*
+ * Gets the install button for a specific item
+ *
+ * @param aItem
+ * The item to get the install button for
+ * @return The install button for aItem
+ */
+function get_install_button(aItem) {
+ isnot(aItem, null, "Item should not be null when checking state of install button");
+ var installStatus = getAnonymousElementByAttribute(aItem, "anonid", "install-status");
+ return getAnonymousElementByAttribute(installStatus, "anonid", "install-remote-btn");
+}
+
+
+// Tests that searching for the empty string does nothing when not in the search view
+add_test(function() {
+ is(gCategoryUtilities.isTypeVisible("search"), false, "Search category should initially be hidden");
+
+ var selectedCategory = gCategoryUtilities.selectedCategory;
+ isnot(selectedCategory, "search", "Selected type should not initially be the search view");
+ search("", true, run_next_test, selectedCategory);
+});
+
+// Tests that the results from a query are sorted by relevancescore in descending order.
+// Also test that double clicking non-install items goes to the detail view, and that
+// only remote items have install buttons showing
+add_test(function() {
+ search(QUERY, false, function() {
+ check_filtered_results(QUERY, "relevancescore", false);
+
+ var list = gManagerWindow.document.getElementById("search-list");
+ var results = get_actual_results();
+ for (var result of results) {
+ var installBtn = get_install_button(result.item);
+ is(installBtn.hidden, result.name.indexOf("remote") != 0,
+ "Install button should only be showing for remote items");
+ }
+
+ var currentIndex = -1;
+ function run_next_double_click_test() {
+ currentIndex++;
+ if (currentIndex >= results.length) {
+ run_next_test();
+ return;
+ }
+
+ var result = results[currentIndex];
+ if (result.name.indexOf("install") == 0) {
+ run_next_double_click_test();
+ return;
+ }
+
+ var item = result.item;
+ list.ensureElementIsVisible(item);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ var name = gManagerWindow.document.getElementById("detail-name").textContent;
+ is(name, item.mAddon.name, "Name in detail view should be correct");
+ var version = gManagerWindow.document.getElementById("detail-version").value;
+ is(version, item.mAddon.version, "Version in detail view should be correct");
+
+ EventUtils.synthesizeMouseAtCenter(gManagerWindow.document.getElementById("category-search"),
+ { }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, run_next_double_click_test);
+ });
+ }
+
+ run_next_double_click_test();
+ });
+});
+
+// Tests that the sorters and filters correctly manipulate the results
+add_test(function() {
+ var sorters = gManagerWindow.document.getElementById("search-sorters");
+ var originalHandler = sorters.handler;
+
+ var sorterNames = ["name", "dateUpdated"];
+ var buttonIds = ["name-btn", "date-btn"];
+ var currentIndex = 0;
+ var currentReversed = false;
+
+ function run_sort_test() {
+ if (currentIndex >= sorterNames.length) {
+ sorters.handler = originalHandler;
+ run_next_test();
+ return;
+ }
+
+ // Simulate clicking on a specific sorter
+ var buttonId = buttonIds[currentIndex];
+ var sorter = getAnonymousElementByAttribute(sorters, "anonid", buttonId);
+ is_element_visible(sorter);
+ EventUtils.synthesizeMouseAtCenter(sorter, { }, gManagerWindow);
+ }
+
+ sorters.handler = {
+ onSortChanged: function(aSortBy, aAscending) {
+ if (originalHandler && "onSortChanged" in originalHandler)
+ originalHandler.onSortChanged(aSortBy, aAscending);
+
+ check_filtered_results(QUERY, sorterNames[currentIndex], currentReversed);
+
+ if (currentReversed)
+ currentIndex++;
+ currentReversed = !currentReversed;
+
+ run_sort_test();
+ }
+ };
+
+ check_filtered_results(QUERY, "relevancescore", false);
+ run_sort_test();
+});
+
+// Tests that searching for the empty string does nothing when in search view
+add_test(function() {
+ search("", true, function() {
+ check_filtered_results(QUERY, "dateUpdated", true);
+ run_next_test();
+ });
+});
+
+// Tests that clicking a different category hides the search query
+add_test(function() {
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.isTypeVisible("search"), false, "Search category should be hidden");
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+ run_next_test();
+ });
+});
+
+// Tests that re-searching for query doesn't actually complete a new search,
+// and the last sort is still used
+add_test(function() {
+ search(QUERY, true, function() {
+ check_filtered_results(QUERY, "dateUpdated", true);
+ run_next_test();
+ });
+});
+
+// Tests that getting zero results works correctly
+add_test(function() {
+ search(NO_MATCH_QUERY, false, function() {
+ check_filtered_results(NO_MATCH_QUERY, "relevancescore", false);
+ run_next_test();
+ });
+});
+
+// Tests that installing a remote add-on works
+add_test(function() {
+ var installBtn = null;
+
+ var listener = {
+ onInstallEnded: function(aInstall, aAddon) {
+ // Don't immediately consider the installed add-on as local because
+ // if the user was filtering out local add-ons, the installed add-on
+ // would vanish. Only consider add-on as local on new searches.
+
+ aInstall.removeListener(this);
+
+ is(installBtn.hidden, true, "Install button should be hidden after install ended");
+ check_filtered_results(QUERY, "relevancescore", false);
+ run_next_test();
+ }
+ }
+
+ search(QUERY, false, function() {
+ var list = gManagerWindow.document.getElementById("search-list");
+ var remoteItem = get_addon_item(REMOTE_TO_INSTALL);
+ list.ensureElementIsVisible(remoteItem);
+
+ installBtn = get_install_button(remoteItem);
+ is(installBtn.hidden, false, "Install button should be showing before install");
+ remoteItem.mAddon.install.addListener(listener);
+ EventUtils.synthesizeMouseAtCenter(installBtn, { }, gManagerWindow);
+ });
+});
+
+// Tests that re-searching for query results in correct results
+add_test(function() {
+ // Select a different category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.isTypeVisible("search"), false, "Search category should be hidden");
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var installBtn = get_install_button(get_addon_item(REMOTE_TO_INSTALL));
+ is(installBtn.hidden, true, "Install button should be hidden for installed item");
+
+ search(QUERY, true, function() {
+ check_filtered_results(QUERY, "relevancescore", false);
+ run_next_test();
+ });
+ });
+});
+
+// Tests that incompatible add-ons are shown with a warning if compatibility checking is disabled
+add_test(function() {
+ AddonManager.checkCompatibility = false;
+ search("incompatible", false, function() {
+ var item = get_addon_item("remote5");
+ is_element_visible(item, "Incompatible addon should be visible");
+ is(item.getAttribute("notification"), "warning", "Compatibility warning should be shown");
+
+ item = get_addon_item("remote6");
+ is(item, null, "Addon incompatible with the product should not be visible");
+
+ AddonManager.checkCompatibility = true;
+ run_next_test();
+ });
+});
+
+// Tests that compatible-by-default addons are shown if strict compatibility checking is disabled
+add_test(function() {
+ restart_manager(gManagerWindow, "addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, false);
+ search("incompatible", false, function() {
+ var item = get_addon_item("remote5");
+ is_element_visible(item, "Incompatible addon should be visible");
+ isnot(item.getAttribute("notification"), "warning", "Compatibility warning should not be shown");
+
+ item = get_addon_item("remote6");
+ is(item, null, "Addon incompatible with the product should not be visible");
+
+ Services.prefs.setBoolPref(PREF_STRICT_COMPAT, true);
+ run_next_test();
+ });
+ });
+});
+
+
+// Tests that restarting the manager doesn't change search results
+add_test(function() {
+ restart_manager(gManagerWindow, null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ // We never restore to the search pane
+ is(gCategoryUtilities.selectedCategory, "discover", "View should have changed to discover");
+
+ // Installed add-on is considered local on new search
+ gAddonInstalled = true;
+
+ // Switch over to extensions list so we can do a new search
+ gCategoryUtilities.openType("extension", function() {
+ search(QUERY, false, function() {
+ check_filtered_results(QUERY, "relevancescore", false);
+
+ var installBtn = get_install_button(get_addon_item(REMOTE_TO_INSTALL));
+ is(installBtn.hidden, true, "Install button should be hidden for installed item");
+
+ run_next_test();
+ });
+ });
+ });
+});
+
+function bug_815120_test_search(aLocalOnly) {
+ restart_manager(gManagerWindow, "addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ // Installed add-on is considered local on new search
+ gAddonInstalled = true;
+
+ // The search buttons should be hidden in the LocalOnly setup
+ var localFilterButton = aWindow.document.getElementById("search-filter-local");
+ is(aLocalOnly, is_hidden(localFilterButton), "Local filter button visibility does not match, aLocalOnly = " + aLocalOnly);
+
+ var remoteFilterButton = aWindow.document.getElementById("search-filter-remote");
+ is(aLocalOnly, is_hidden(remoteFilterButton), "Remote filter button visibility does not match, aLocalOnly = " + aLocalOnly);
+
+ search(QUERY, false, function() {
+ check_filtered_results(QUERY, "relevancescore", false, aLocalOnly);
+ run_next_test();
+ });
+ });
+}
+
+// Tests for Bug 815120
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, false);
+ bug_815120_test_search(true);
+});
+
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_XPI_ENABLED, true);
+ bug_815120_test_search(false);
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_searching.xml b/toolkit/mozapps/extensions/test/browser/browser_searching.xml
new file mode 100644
index 000000000..a3537b269
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_searching.xml
@@ -0,0 +1,277 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>FAIL</name>
+ <type id='1'>Extension</type>
+ <guid>addon1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Addon already installed - SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/addon1.xpi</install>
+ </addon>
+ <addon>
+ <name>FAIL</name>
+ <type id='9'>lightweight theme</type>
+ <guid>addon12345@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Addon with uninstallable type shouldn't be visible in search</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/addon1.xpi</install>
+ </addon>
+ <addon>
+ <name>FAIL</name>
+ <type id='1'>Extension</type>
+ <guid>install1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Install already exists - SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/install1.xpi</install>
+ </addon>
+ <addon>
+ <name>PASS - b</name>
+ <type id='1'>Extension</type>
+ <guid>remote1@tests.mozilla.org</guid>
+ <version>3.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary - SEARCH SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="2">http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_searching.xpi</install>
+ </addon>
+ <addon>
+ <name>PASS - d</name>
+ <type id='1'>Extension</type>
+ <guid>remote2@tests.mozilla.org</guid>
+ <version>4.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary - SEARCHing SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="5">http://example.com/remote2.xpi</install>
+ </addon>
+ <addon>
+ <name>PASS - e</name>
+ <type id='1'>Extension</type>
+ <guid>remote3@tests.mozilla.org</guid>
+ <version>5.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary - Does not match query</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/remote3.xpi</install>
+ </addon>
+ <addon>
+ <name>PASS - h</name>
+ <type id='1'>Extension</type>
+ <guid>remote4@tests.mozilla.org</guid>
+ <version>6.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Test summary - SEARCHing SEARCH SEARCH</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="4">http://example.com/remote4.xpi</install>
+ </addon>
+ <addon>
+ <name>PASS - i</name>
+ <type id='1'>Extension</type>
+ <guid>remote5@tests.mozilla.org</guid>
+ <version>6.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Incompatible test</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>1</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/addon1.xpi</install>
+ </addon>
+ <addon>
+ <name>FAIL - j</name>
+ <type id='1'>Extension</type>
+ <guid>remote6@tests.mozilla.org</guid>
+ <version>6.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Incompatible test</summary>
+ <description>Test description</description>
+ <compatible_applications>
+ <application>
+ <name>Fake Product</name>
+ <appID>fakeproduct@mozilla.org</appID>
+ <min_version>0</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="1">http://example.com/addon1.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_searching_empty.xml b/toolkit/mozapps/extensions/test/browser/browser_searching_empty.xml
new file mode 100644
index 000000000..24f6cb89f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_searching_empty.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100" />
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sorting.js b/toolkit/mozapps/extensions/test/browser/browser_sorting.js
new file mode 100644
index 000000000..b57e7a6f7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sorting.js
@@ -0,0 +1,372 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that sorting of add-ons works correctly
+// (this test uses the list view, even though it no longer has sort buttons - see bug 623207)
+
+var gManagerWindow;
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+ gProvider.createAddons([{
+ // enabledInstalled group
+ // * Enabled
+ // * Incompatible but enabled because compatibility checking is off
+ // * Waiting to be installed
+ // * Waiting to be enabled
+ id: "test1@tests.mozilla.org",
+ name: "Test add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 2, 0, 0, 0),
+ size: 1,
+ pendingOperations: AddonManager.PENDING_NONE,
+ }, {
+ id: "test2@tests.mozilla.org",
+ name: "a first add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 1, 23, 59, 59),
+ size: 265,
+ pendingOperations: AddonManager.PENDING_UPGRADE,
+ isActive: true,
+ isCompatible: false,
+ }, {
+ id: "test3@tests.mozilla.org",
+ name: "\u010Cesk\u00FD slovn\u00EDk", // Český slovník
+ description: "foo",
+ updateDate: new Date(2010, 4, 2, 0, 0, 1),
+ size: 12,
+ pendingOperations: AddonManager.PENDING_INSTALL,
+ isActive: false,
+ }, {
+ id: "test4@tests.mozilla.org",
+ name: "canadian dictionary",
+ updateDate: new Date(1970, 0, 1, 0, 0, 0),
+ description: "foo",
+ isActive: true,
+ }, {
+ id: "test5@tests.mozilla.org",
+ name: "croatian dictionary",
+ description: "foo",
+ updateDate: new Date(2012, 12, 12, 0, 0, 0),
+ size: 5,
+ pendingOperations: AddonManager.PENDING_ENABLE,
+ isActive: false,
+ }, {
+ // pendingDisable group
+ // * Waiting to be disabled
+ id: "test6@tests.mozilla.org",
+ name: "orange Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 2, 0, 0, 0),
+ size: 142,
+ isCompatible: false,
+ isActive: true,
+ pendingOperations: AddonManager.PENDING_DISABLE,
+ }, {
+ id: "test7@tests.mozilla.org",
+ name: "Blue Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 1, 23, 59, 59),
+ size: 65,
+ isActive: true,
+ pendingOperations: AddonManager.PENDING_DISABLE,
+ }, {
+ id: "test8@tests.mozilla.org",
+ name: "Green Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 3, 0, 0, 1),
+ size: 125,
+ pendingOperations: AddonManager.PENDING_DISABLE,
+ }, {
+ id: "test9@tests.mozilla.org",
+ name: "red Add-on",
+ updateDate: new Date(2011, 4, 1, 0, 0, 0),
+ description: "foo",
+ isCompatible: false,
+ pendingOperations: AddonManager.PENDING_DISABLE,
+ }, {
+ id: "test10@tests.mozilla.org",
+ name: "Purple Add-on",
+ description: "foo",
+ updateDate: new Date(2012, 12, 12, 0, 0, 0),
+ size: 56,
+ isCompatible: false,
+ pendingOperations: AddonManager.PENDING_DISABLE,
+ }, {
+ // pendingUninstall group
+ // * Waiting to be removed
+ id: "test11@tests.mozilla.org",
+ name: "amber Add-on",
+ description: "foo",
+ updateDate: new Date(1978, 4, 2, 0, 0, 0),
+ size: 142,
+ isActive: false,
+ appDisabled: true,
+ pendingOperations: AddonManager.PENDING_UNINSTALL,
+ }, {
+ id: "test12@tests.mozilla.org",
+ name: "Salmon Add-on - pending disable",
+ description: "foo",
+ updateDate: new Date(2054, 4, 1, 23, 59, 59),
+ size: 65,
+ isActive: true,
+ pendingOperations: AddonManager.PENDING_UNINSTALL,
+ }, {
+ id: "test13@tests.mozilla.org",
+ name: "rose Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 2, 0, 0, 1),
+ size: 125,
+ isActive: false,
+ userDisabled: true,
+ pendingOperations: AddonManager.PENDING_UNINSTALL,
+ }, {
+ id: "test14@tests.mozilla.org",
+ name: "Violet Add-on",
+ updateDate: new Date(2010, 5, 1, 0, 0, 0),
+ description: "foo",
+ isActive: false,
+ appDisabled: true,
+ pendingOperations: AddonManager.PENDING_UNINSTALL,
+ }, {
+ id: "test15@tests.mozilla.org",
+ name: "white Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 12, 0, 0, 0),
+ size: 56,
+ isActive: false,
+ userDisabled: true,
+ pendingOperations: AddonManager.PENDING_UNINSTALL,
+ }, {
+ // disabledIncompatibleBlocked group
+ // * Disabled
+ // * Incompatible
+ // * Blocklisted
+ id: "test16@tests.mozilla.org",
+ name: "grimsby Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 1, 0, 0, 0),
+ size: 142,
+ isActive: false,
+ appDisabled: true,
+ }, {
+ id: "test17@tests.mozilla.org",
+ name: "beamsville Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 8, 23, 59, 59),
+ size: 65,
+ isActive: false,
+ userDisabled: true,
+ }, {
+ id: "test18@tests.mozilla.org",
+ name: "smithville Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 3, 0, 0, 1),
+ size: 125,
+ isActive: false,
+ userDisabled: true,
+ blocklistState: Ci.nsIBlocklistService.STATE_OUTDATED,
+ }, {
+ id: "test19@tests.mozilla.org",
+ name: "dunnville Add-on",
+ updateDate: new Date(2010, 4, 2, 0, 0, 0),
+ description: "foo",
+ isActive: false,
+ appDisabled: true,
+ isCompatible: false,
+ blocklistState: Ci.nsIBlocklistService.STATE_NOT_BLOCKED,
+ }, {
+ id: "test20@tests.mozilla.org",
+ name: "silverdale Add-on",
+ description: "foo",
+ updateDate: new Date(2010, 4, 12, 0, 0, 0),
+ size: 56,
+ isActive: false,
+ appDisabled: true,
+ blocklistState: Ci.nsIBlocklistService.STATE_BLOCKED,
+ }]);
+
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+function set_order(aSortBy, aAscending) {
+ var list = gManagerWindow.document.getElementById("addon-list");
+ var elements = [];
+ var node = list.firstChild;
+ while (node) {
+ elements.push(node);
+ node = node.nextSibling;
+ }
+ gManagerWindow.sortElements(elements, ["uiState", aSortBy], aAscending);
+ for (let element of elements)
+ list.appendChild(element);
+}
+
+function check_order(aExpectedOrder) {
+ var order = [];
+ var list = gManagerWindow.document.getElementById("addon-list");
+ var node = list.firstChild;
+ while (node) {
+ var id = node.getAttribute("value");
+ if (id && id.endsWith("@tests.mozilla.org"))
+ order.push(node.getAttribute("value"));
+ node = node.nextSibling;
+ }
+
+ is(order.toSource(), aExpectedOrder.toSource(), "Should have seen the right order");
+}
+
+// Tests that ascending name ordering was the default
+add_test(function() {
+
+ check_order([
+ "test2@tests.mozilla.org",
+ "test4@tests.mozilla.org",
+ "test3@tests.mozilla.org",
+ "test5@tests.mozilla.org",
+ "test1@tests.mozilla.org",
+ "test7@tests.mozilla.org",
+ "test8@tests.mozilla.org",
+ "test6@tests.mozilla.org",
+ "test10@tests.mozilla.org",
+ "test9@tests.mozilla.org",
+ "test11@tests.mozilla.org",
+ "test13@tests.mozilla.org",
+ "test12@tests.mozilla.org",
+ "test14@tests.mozilla.org",
+ "test15@tests.mozilla.org",
+ "test17@tests.mozilla.org",
+ "test19@tests.mozilla.org",
+ "test16@tests.mozilla.org",
+ "test20@tests.mozilla.org",
+ "test18@tests.mozilla.org",
+ ]);
+ run_next_test();
+});
+
+// Tests that switching to date ordering works
+add_test(function() {
+ set_order("updateDate", false);
+
+ // When we're ascending with updateDate, it's from newest
+ // to oldest.
+
+ check_order([
+ "test5@tests.mozilla.org",
+ "test3@tests.mozilla.org",
+ "test1@tests.mozilla.org",
+ "test2@tests.mozilla.org",
+ "test4@tests.mozilla.org",
+ "test10@tests.mozilla.org",
+ "test9@tests.mozilla.org",
+ "test8@tests.mozilla.org",
+ "test6@tests.mozilla.org",
+ "test7@tests.mozilla.org",
+ "test12@tests.mozilla.org",
+ "test14@tests.mozilla.org",
+ "test15@tests.mozilla.org",
+ "test13@tests.mozilla.org",
+ "test11@tests.mozilla.org",
+ "test20@tests.mozilla.org",
+ "test17@tests.mozilla.org",
+ "test18@tests.mozilla.org",
+ "test19@tests.mozilla.org",
+ "test16@tests.mozilla.org",
+ ]);
+
+ set_order("updateDate", true);
+
+ check_order([
+ "test4@tests.mozilla.org",
+ "test2@tests.mozilla.org",
+ "test1@tests.mozilla.org",
+ "test3@tests.mozilla.org",
+ "test5@tests.mozilla.org",
+ "test7@tests.mozilla.org",
+ "test6@tests.mozilla.org",
+ "test8@tests.mozilla.org",
+ "test9@tests.mozilla.org",
+ "test10@tests.mozilla.org",
+ "test11@tests.mozilla.org",
+ "test13@tests.mozilla.org",
+ "test15@tests.mozilla.org",
+ "test14@tests.mozilla.org",
+ "test12@tests.mozilla.org",
+ "test16@tests.mozilla.org",
+ "test19@tests.mozilla.org",
+ "test18@tests.mozilla.org",
+ "test17@tests.mozilla.org",
+ "test20@tests.mozilla.org",
+ ]);
+
+ run_next_test();
+});
+
+// Tests that switching to name ordering works
+add_test(function() {
+ set_order("name", true);
+
+ check_order([
+ "test2@tests.mozilla.org",
+ "test4@tests.mozilla.org",
+ "test3@tests.mozilla.org",
+ "test5@tests.mozilla.org",
+ "test1@tests.mozilla.org",
+ "test7@tests.mozilla.org",
+ "test8@tests.mozilla.org",
+ "test6@tests.mozilla.org",
+ "test10@tests.mozilla.org",
+ "test9@tests.mozilla.org",
+ "test11@tests.mozilla.org",
+ "test13@tests.mozilla.org",
+ "test12@tests.mozilla.org",
+ "test14@tests.mozilla.org",
+ "test15@tests.mozilla.org",
+ "test17@tests.mozilla.org",
+ "test19@tests.mozilla.org",
+ "test16@tests.mozilla.org",
+ "test20@tests.mozilla.org",
+ "test18@tests.mozilla.org",
+ ]);
+
+ set_order("name", false);
+
+ check_order([
+ "test1@tests.mozilla.org",
+ "test5@tests.mozilla.org",
+ "test3@tests.mozilla.org",
+ "test4@tests.mozilla.org",
+ "test2@tests.mozilla.org",
+ "test9@tests.mozilla.org",
+ "test10@tests.mozilla.org",
+ "test6@tests.mozilla.org",
+ "test8@tests.mozilla.org",
+ "test7@tests.mozilla.org",
+ "test15@tests.mozilla.org",
+ "test14@tests.mozilla.org",
+ "test12@tests.mozilla.org",
+ "test13@tests.mozilla.org",
+ "test11@tests.mozilla.org",
+ "test18@tests.mozilla.org",
+ "test20@tests.mozilla.org",
+ "test16@tests.mozilla.org",
+ "test19@tests.mozilla.org",
+ "test17@tests.mozilla.org",
+ ]);
+
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_sorting_plugins.js b/toolkit/mozapps/extensions/test/browser/browser_sorting_plugins.js
new file mode 100644
index 000000000..2bb6b4ba4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_sorting_plugins.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that sorting of plugins works correctly
+// (this test checks that plugins with "ask to activate" state appear after those with
+// "always activate" and before those with "never activate")
+
+var gManagerWindow;
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+ gProvider.createAddons([{
+ // enabledInstalled group
+ // * Always activate
+ // * Ask to activate
+ // * Never activate
+ id: "test1@tests.mozilla.org",
+ name: "Java Applet Plug-in Java 7 Update 51",
+ description: "foo",
+ type: "plugin",
+ isActive: true,
+ userDisabled: AddonManager.STATE_ASK_TO_ACTIVATE
+ }, {
+ id: "test2@tests.mozilla.org",
+ name: "Quick Time Plug-in",
+ description: "foo",
+ type: "plugin",
+ isActive: true,
+ userDisabled: false
+ }, {
+ id: "test3@tests.mozilla.org",
+ name: "Shockwave Flash",
+ description: "foo",
+ type: "plugin",
+ isActive: false,
+ userDisabled: true
+ }, {
+ id: "test4@tests.mozilla.org",
+ name: "Adobe Reader Plug-in",
+ description: "foo",
+ type: "plugin",
+ isActive: true,
+ userDisabled: AddonManager.STATE_ASK_TO_ACTIVATE
+ }, {
+ id: "test5@tests.mozilla.org",
+ name: "3rd Party Plug-in",
+ description: "foo",
+ type: "plugin",
+ isActive: true,
+ userDisabled: false
+ }]);
+
+ open_manager("addons://list/plugin", function(aWindow) {
+ gManagerWindow = aWindow;
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+function check_order(aExpectedOrder) {
+ var order = [];
+ var list = gManagerWindow.document.getElementById("addon-list");
+ var node = list.firstChild;
+ while (node) {
+ var id = node.getAttribute("value");
+ if (id && id.endsWith("@tests.mozilla.org"))
+ order.push(node.getAttribute("value"));
+ node = node.nextSibling;
+ }
+
+ is(order.toSource(), aExpectedOrder.toSource(), "Should have seen the right order");
+}
+
+// Tests that ascending name ordering was the default
+add_test(function() {
+
+ check_order([
+ "test5@tests.mozilla.org",
+ "test2@tests.mozilla.org",
+ "test4@tests.mozilla.org",
+ "test1@tests.mozilla.org",
+ "test3@tests.mozilla.org"
+ ]);
+
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_system_addons_are_e10s.js b/toolkit/mozapps/extensions/test/browser/browser_system_addons_are_e10s.js
new file mode 100644
index 000000000..37ab76542
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_system_addons_are_e10s.js
@@ -0,0 +1,13 @@
+"use strict";
+
+add_task(function* test_enabled() {
+ let addons = yield new Promise(resolved => AddonManager.getAllAddons(resolved));
+ for (let addon of addons) {
+ if (addon.isSystem) {
+ ok(addon.multiprocessCompatible,
+ `System addon ${addon.id} is not marked as multiprocess compatible`);
+ ok(!addon.unpack,
+ `System add-on ${addon.id} isn't a packed add-on.`);
+ }
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_tabsettings.js b/toolkit/mozapps/extensions/test/browser/browser_tabsettings.js
new file mode 100644
index 000000000..2838698c7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_tabsettings.js
@@ -0,0 +1,100 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests various aspects of the details view
+
+var gManagerWindow;
+var gCategoryUtilities;
+var gProvider;
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "tabsettings@tests.mozilla.org",
+ name: "Tab Settings",
+ version: "1",
+ optionsURL: CHROMEROOT + "addon_prefs.xul",
+ optionsType: AddonManager.OPTIONS_TYPE_TAB
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+add_test(function() {
+ var addon = get_addon_element(gManagerWindow, "tabsettings@tests.mozilla.org");
+ is(addon.mAddon.optionsType, AddonManager.OPTIONS_TYPE_TAB, "Options should be inline type");
+ addon.parentNode.ensureElementIsVisible(addon);
+
+ var button = gManagerWindow.document.getAnonymousElementByAttribute(addon, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ if (gUseInContentUI) {
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+
+ var browser = gBrowser.selectedBrowser;
+ browser.addEventListener("DOMContentLoaded", function() {
+ browser.removeEventListener("DOMContentLoaded", arguments.callee, false);
+ is(browser.currentURI.spec, addon.mAddon.optionsURL, "New tab should have loaded the options URL");
+ browser.contentWindow.close();
+ run_next_test();
+ }, false);
+ return;
+ }
+
+ let instantApply = Services.prefs.getBoolPref("browser.preferences.instantApply");
+
+ function observer(aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "domwindowclosed":
+ // Give the preference window a chance to finish closing before
+ // closing the add-ons manager.
+ waitForFocus(function () {
+ Services.ww.unregisterNotification(observer);
+ run_next_test();
+ });
+ break;
+ case "domwindowopened":
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+ waitForFocus(function () {
+ // If the openDialog privileges are wrong a new browser window
+ // will open, let the test proceed (and fail) rather than timeout.
+ if (win.location != addon.mAddon.optionsURL &&
+ win.location != "chrome://browser/content/browser.xul")
+ return;
+
+ is(win.location, addon.mAddon.optionsURL,
+ "The correct addon pref window should have opened");
+
+ let chromeFlags = win.QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIWebNavigation).
+ QueryInterface(Ci.nsIDocShellTreeItem).treeOwner.
+ QueryInterface(Ci.nsIInterfaceRequestor).
+ getInterface(Ci.nsIXULWindow).chromeFlags;
+ ok(chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_CHROME &&
+ (instantApply || chromeFlags & Ci.nsIWebBrowserChrome.CHROME_OPENAS_DIALOG),
+ "Window was open as a chrome dialog.");
+
+ win.close();
+ }, win);
+ break;
+ }
+ }
+
+ Services.ww.registerNotification(observer);
+ EventUtils.synthesizeMouseAtCenter(button, { clickCount: 1 }, gManagerWindow);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
new file mode 100644
index 000000000..5ff2aff78
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_task_next_test.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we throw if a test created with add_task()
+// calls run_next_test
+
+add_task(function* run_next_throws() {
+ let err = null;
+ try {
+ run_next_test();
+ } catch (e) {
+ err = e;
+ info("run_next_test threw " + err);
+ }
+ ok(err, "run_next_test() should throw an error inside an add_task test");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_types.js b/toolkit/mozapps/extensions/test/browser/browser_types.js
new file mode 100644
index 000000000..8abb0ff73
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_types.js
@@ -0,0 +1,473 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that registering new types works
+
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gProvider = {
+};
+
+var gTypes = [
+ new AddonManagerPrivate.AddonType("type1", null, "Type 1",
+ AddonManager.VIEW_TYPE_LIST, 4500),
+ new AddonManagerPrivate.AddonType("missing1", null, "Missing 1"),
+ new AddonManagerPrivate.AddonType("type2", null, "Type 1",
+ AddonManager.VIEW_TYPE_LIST, 5100,
+ AddonManager.TYPE_UI_HIDE_EMPTY),
+ {
+ id: "type3",
+ name: "Type 3",
+ uiPriority: 5200,
+ viewType: AddonManager.VIEW_TYPE_LIST
+ }
+];
+
+function go_back(aManager) {
+ if (gUseInContentUI) {
+ gBrowser.goBack();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("back-btn"),
+ { }, aManager);
+ }
+}
+
+function go_forward(aManager) {
+ if (gUseInContentUI) {
+ gBrowser.goForward();
+ } else {
+ EventUtils.synthesizeMouseAtCenter(aManager.document.getElementById("forward-btn"),
+ { }, aManager);
+ }
+}
+
+function check_state(aManager, canGoBack, canGoForward) {
+ var doc = aManager.document;
+
+ if (gUseInContentUI) {
+ is(gBrowser.canGoBack, canGoBack, "canGoBack should be correct");
+ is(gBrowser.canGoForward, canGoForward, "canGoForward should be correct");
+ }
+
+ if (!is_hidden(doc.getElementById("back-btn"))) {
+ is(!doc.getElementById("back-btn").disabled, canGoBack, "Back button should have the right state");
+ is(!doc.getElementById("forward-btn").disabled, canGoForward, "Forward button should have the right state");
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ run_next_test();
+}
+
+function end_test() {
+ finish();
+}
+
+// Add a new type, open the manager and make sure it is in the right place
+add_test(function() {
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.get("type2"), "Type 2 should be present");
+ ok(!gCategoryUtilities.get("missing1", true), "Missing 1 should be absent");
+
+ is(gCategoryUtilities.get("type1").previousSibling.getAttribute("value"),
+ "addons://list/extension", "Type 1 should be in the right place");
+ is(gCategoryUtilities.get("type2").previousSibling.getAttribute("value"),
+ "addons://list/theme", "Type 2 should be in the right place");
+
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+ ok(!gCategoryUtilities.isTypeVisible("type2"), "Type 2 should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Select the type, close the manager and remove it then open the manager and
+// check we're back to the default view
+add_test(function() {
+ gCategoryUtilities.openType("type1", function() {
+ close_manager(gManagerWindow, function() {
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should be absent");
+ ok(!gCategoryUtilities.get("type2", true), "Type 2 should be absent");
+ ok(!gCategoryUtilities.get("missing1", true), "Missing 1 should be absent");
+
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be back to the default view");
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+});
+
+// Add a type while the manager is still open and check it appears
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should be absent");
+ ok(!gCategoryUtilities.get("type2", true), "Type 2 should be absent");
+ ok(!gCategoryUtilities.get("missing1", true), "Missing 1 should be absent");
+
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.get("type2"), "Type 2 should be present");
+ ok(!gCategoryUtilities.get("missing1", true), "Missing 1 should be absent");
+
+ is(gCategoryUtilities.get("type1").previousSibling.getAttribute("value"),
+ "addons://list/extension", "Type 1 should be in the right place");
+ is(gCategoryUtilities.get("type2").previousSibling.getAttribute("value"),
+ "addons://list/theme", "Type 2 should be in the right place");
+
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+ ok(!gCategoryUtilities.isTypeVisible("type2"), "Type 2 should be hidden");
+
+ run_next_test();
+ });
+});
+
+// Remove the type while it is beng viewed and check it is replaced with the
+// default view
+add_test(function() {
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("plugin", function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "type1", "Should be showing the custom view");
+ check_state(gManagerWindow, true, true);
+
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should be absent");
+ ok(!gCategoryUtilities.get("type2", true), "Type 2 should be absent");
+ ok(!gCategoryUtilities.get("missing1", true), "Missing 1 should be absent");
+
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be back to the default view");
+ check_state(gManagerWindow, true, true);
+
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be showing the extension view");
+ check_state(gManagerWindow, false, true);
+
+ go_forward(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be back to the default view");
+ check_state(gManagerWindow, true, true);
+
+ go_forward(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "Should be back to the plugins view");
+ check_state(gManagerWindow, true, false);
+
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be back to the default view");
+ check_state(gManagerWindow, true, true);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Test that when going back to a now missing category we skip it
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("plugin", function() {
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be back to the first view");
+ check_state(gManagerWindow, false, true);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Test that when going forward to a now missing category we skip it
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("plugin", function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be back to the extension view");
+
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_forward(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "Should be back to the plugin view");
+ check_state(gManagerWindow, true, false);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Test that when going back to a now missing category and we can't go back any
+// any further then we just display the default view
+add_test(function() {
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ open_manager("addons://list/type1", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "type1", "Should be at the custom view");
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("extension", function() {
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be at the default view");
+ check_state(gManagerWindow, false, true);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+});
+
+// Test that when going forward to a now missing category and we can't go
+// forward any further then we just display the default view
+add_test(function() {
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be at the extension view");
+
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_forward(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be at the default view");
+ check_state(gManagerWindow, true, false);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Test that when going back we skip multiple missing categories
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("type3", function() {
+ gCategoryUtilities.openType("plugin", function() {
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be back to the first view");
+ check_state(gManagerWindow, false, true);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+ });
+});
+
+// Test that when going forward we skip multiple missing categories
+add_test(function() {
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("type3", function() {
+ gCategoryUtilities.openType("plugin", function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ go_back(gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be back to the extension view");
+
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_forward(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "Should be back to the plugin view");
+ check_state(gManagerWindow, true, false);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+ });
+});
+
+// Test that when going back we skip all missing categories and when we can't go
+// back any any further then we just display the default view
+add_test(function() {
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ open_manager("addons://list/type1", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ is(gCategoryUtilities.selectedCategory, "type1", "Should be at the custom view");
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type3", function() {
+ gCategoryUtilities.openType("extension", function() {
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be at the default view");
+ check_state(gManagerWindow, false, true);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+});
+
+// Test that when going forward we skip all missing categories and when we can't
+// go back any any further then we just display the default view
+add_test(function() {
+ AddonManagerPrivate.registerProvider(gProvider, gTypes);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+
+ ok(gCategoryUtilities.get("type1"), "Type 1 should be present");
+ ok(gCategoryUtilities.isTypeVisible("type1"), "Type 1 should be visible");
+
+ gCategoryUtilities.openType("type1", function() {
+ gCategoryUtilities.openType("type3", function() {
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ go_back(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "Should be at the extension view");
+
+ AddonManagerPrivate.unregisterProvider(gProvider);
+
+ ok(!gCategoryUtilities.get("type1", true), "Type 1 should not be present");
+
+ go_forward(gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "discover", "Should be at the default view");
+ check_state(gManagerWindow, true, false);
+
+ close_manager(gManagerWindow, run_next_test);
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_uninstalling.js b/toolkit/mozapps/extensions/test/browser/browser_uninstalling.js
new file mode 100644
index 000000000..a9329e496
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_uninstalling.js
@@ -0,0 +1,1098 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that searching for add-ons works correctly
+
+var gManagerWindow;
+var gDocument;
+var gCategoryUtilities;
+var gProvider;
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Uninstall needs restart",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_UNINSTALL
+ }, {
+ id: "addon2@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 1",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon3@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 2",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon4@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 3",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon5@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 4",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon6@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 5",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon7@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 6",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon8@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 7",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }, {
+ id: "addon9@tests.mozilla.org",
+ name: "Uninstall doesn't need restart 8",
+ type: "extension",
+ operationsRequiringRestart: AddonManager.OP_NEEDS_RESTART_NONE
+ }]);
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gDocument = gManagerWindow.document;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+function get_item_in_list(aId, aList) {
+ var item = aList.firstChild;
+ while (item) {
+ if ("mAddon" in item && item.mAddon.id == aId) {
+ aList.ensureElementIsVisible(item);
+ return item;
+ }
+ item = item.nextSibling;
+ }
+ return null;
+}
+
+// Tests that uninstalling a normal add-on from the list view can be undone
+add_test(function() {
+ var ID = "addon1@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ ok(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL, "Add-on should require a restart to uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a restartless add-on from the list view can be undone
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(aAddon.isActive, "Add-on should be active");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a disabled restartless add-on from the list view can
+// be undone and doesn't re-enable
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ aAddon.userDisabled = true;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ aAddon.userDisabled = false;
+ ok(aAddon.isActive, "Add-on should be active");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a normal add-on from the search view can be undone
+add_test(function() {
+ var ID = "addon1@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ ok(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL, "Add-on should require a restart to uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a restartless add-on from the search view can be undone
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(aAddon.isActive, "Add-on should be active");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a disabled restartless add-on from the search view can
+// be undone and doesn't re-enable
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ aAddon.userDisabled = true;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ aAddon.userDisabled = false;
+ ok(aAddon.isActive, "Add-on should be active");
+
+ run_next_test();
+ });
+ });
+});
+
+// Tests that uninstalling a normal add-on from the details view switches back
+// to the list view and can be undone
+add_test(function() {
+ var ID = "addon1@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ ok(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL, "Add-on should require a restart to uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(get_current_view(gManagerWindow).id, "detail-view", "Should be in the detail view");
+
+ var button = gDocument.getElementById("detail-uninstall-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
+
+// Tests that uninstalling a restartless add-on from the details view switches
+// back to the list view and can be undone
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(get_current_view(gManagerWindow).id, "detail-view", "Should be in the detail view");
+
+ var button = gDocument.getElementById("detail-uninstall-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(aAddon.isActive, "Add-on should be active");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
+
+// Tests that uninstalling a restartless add-on from the details view switches
+// back to the list view and can be undone and doesn't re-enable
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ aAddon.userDisabled = true;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 1 }, gManagerWindow);
+ EventUtils.synthesizeMouseAtCenter(item, { clickCount: 2 }, gManagerWindow);
+ wait_for_view_load(gManagerWindow, function() {
+ is(get_current_view(gManagerWindow).id, "detail-view", "Should be in the detail view");
+
+ var button = gDocument.getElementById("detail-uninstall-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ // Force XBL to apply
+ item.clientTop;
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ ok(!aAddon.isActive, "Add-on should be inactive");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ aAddon.userDisabled = false;
+ ok(aAddon.isActive, "Add-on should be active");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
+
+// Tests that a normal add-on pending uninstall shows up in the list view
+add_test(function() {
+ var ID = "addon1@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ ok(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL, "Add-on should require a restart to uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ gCategoryUtilities.openType("plugin", function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "View should have changed to plugin");
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
+
+// Tests that a normal add-on pending uninstall shows up in the search view
+add_test(function() {
+ var ID = "addon1@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ ok(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL, "Add-on should require a restart to uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ gCategoryUtilities.openType("plugin", function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "View should have changed to plugin");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(!!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should be pending uninstall");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(!button.hidden, "Restart button should not be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+});
+
+// Tests that switching away from the list view finalises the uninstall of
+// multiple restartless add-ons
+add_test(function() {
+ var ID = "addon2@tests.mozilla.org";
+ var ID2 = "addon6@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ item = get_item_in_list(ID2, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ gCategoryUtilities.openType("plugin", function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "View should have changed to extension");
+
+ AddonManager.getAddonsByIDs([ID, ID2], function([aAddon, aAddon2]) {
+ is(aAddon, null, "Add-on should no longer be installed");
+ is(aAddon2, null, "Second add-on should no longer be installed");
+
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ is(item, null, "Should not have found the add-on in the list");
+ item = get_item_in_list(ID2, list);
+ is(item, null, "Should not have found the second add-on in the list");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that switching away from the search view finalises the uninstall of
+// multiple restartless add-ons
+add_test(function() {
+ var ID = "addon3@tests.mozilla.org";
+ var ID2 = "addon7@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ item = get_item_in_list(ID2, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ gCategoryUtilities.openType("plugin", function() {
+ is(gCategoryUtilities.selectedCategory, "plugin", "View should have changed to extension");
+
+ AddonManager.getAddonsByIDs([ID, ID2], function([aAddon, aAddon2]) {
+ is(aAddon, null, "Add-on should no longer be installed");
+ is(aAddon2, null, "Second add-on should no longer be installed");
+
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ var item = get_item_in_list(ID, list);
+ is(item, null, "Should not have found the add-on in the list");
+ item = get_item_in_list(ID2, list);
+ is(item, null, "Should not have found the second add-on in the list");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that closing the manager from the list view finalises the uninstall of
+// multiple restartless add-ons
+add_test(function() {
+ var ID = "addon4@tests.mozilla.org";
+ var ID2 = "addon8@tests.mozilla.org";
+ var list = gDocument.getElementById("addon-list");
+
+ // Select the extensions category
+ gCategoryUtilities.openType("extension", function() {
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ item = get_item_in_list(ID2, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ close_manager(gManagerWindow, function() {
+ AddonManager.getAddonsByIDs([ID, ID2], function([aAddon, aAddon2]) {
+ is(aAddon, null, "Add-on should no longer be installed");
+ is(aAddon2, null, "Second add-on should no longer be installed");
+
+ open_manager(null, function(aWindow) {
+ gManagerWindow = aWindow;
+ gDocument = gManagerWindow.document;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ var list = gDocument.getElementById("addon-list");
+
+ is(gCategoryUtilities.selectedCategory, "extension", "View should have changed to extension");
+
+ var item = get_item_in_list(ID, list);
+ is(item, null, "Should not have found the add-on in the list");
+ item = get_item_in_list(ID2, list);
+ is(item, null, "Should not have found the second add-on in the list");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+});
+
+// Tests that closing the manager from the search view finalises the uninstall
+// of multiple restartless add-ons
+add_test(function() {
+ var ID = "addon5@tests.mozilla.org";
+ var ID2 = "addon9@tests.mozilla.org";
+ var list = gDocument.getElementById("search-list");
+
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ // Make sure to show local add-ons
+ EventUtils.synthesizeMouseAtCenter(gDocument.getElementById("search-filter-local"), { }, gManagerWindow);
+
+ AddonManager.getAddonByID(ID, function(aAddon) {
+ ok(aAddon.isActive, "Add-on should be active");
+ ok(!(aAddon.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL), "Add-on should not require a restart to uninstall");
+ ok(!(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL), "Add-on should not be pending uninstall");
+
+ var item = get_item_in_list(ID, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ var button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ // Force XBL to apply
+ item.clientTop;
+
+ is(item.getAttribute("pending"), "uninstall", "Add-on should be uninstalling");
+
+ ok(aAddon.pendingOperations & AddonManager.PENDING_UNINSTALL, "Add-on should be pending uninstall");
+ ok(!aAddon.isActive, "Add-on should be inactive");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "restart-btn");
+ isnot(button, null, "Should have a restart button");
+ ok(button.hidden, "Restart button should be hidden");
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "undo-btn");
+ isnot(button, null, "Should have an undo button");
+
+ item = get_item_in_list(ID2, list);
+ isnot(item, null, "Should have found the add-on in the list");
+
+ button = gDocument.getAnonymousElementByAttribute(item, "anonid", "remove-btn");
+ isnot(button, null, "Should have a remove button");
+ ok(!button.disabled, "Button should not be disabled");
+
+ EventUtils.synthesizeMouseAtCenter(button, { }, gManagerWindow);
+
+ close_manager(gManagerWindow, function() {
+ AddonManager.getAddonsByIDs([ID, ID2], function([aAddon, aAddon2]) {
+ is(aAddon, null, "Add-on should no longer be installed");
+ is(aAddon2, null, "Second add-on should no longer be installed");
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gDocument = gManagerWindow.document;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ var list = gDocument.getElementById("search-list");
+ var searchBox = gManagerWindow.document.getElementById("header-search");
+
+ searchBox.value = "Uninstall";
+
+ EventUtils.synthesizeMouseAtCenter(searchBox, { }, gManagerWindow);
+ EventUtils.synthesizeKey("VK_RETURN", { }, gManagerWindow);
+
+ wait_for_view_load(gManagerWindow, function() {
+ is(gCategoryUtilities.selectedCategory, "search", "View should have changed to search");
+
+ var item = get_item_in_list(ID, list);
+ is(item, null, "Should not have found the add-on in the list");
+ item = get_item_in_list(ID2, list);
+ is(item, null, "Should not have found the second add-on in the list");
+
+ run_next_test();
+ });
+ });
+ });
+ });
+ });
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_update.js b/toolkit/mozapps/extensions/test/browser/browser_update.js
new file mode 100644
index 000000000..09ceb1240
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_update.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that updates correctly flush caches and that new files gets updated.
+
+function test() {
+ requestLongerTimeout(2);
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+ run_next_test();
+}
+
+// Install a first version
+add_test(function() {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_update1_1.xpi",
+ function(aInstall) {
+ aInstall.install();
+ }, "application/x-xpinstall");
+
+ Services.ppmm.addMessageListener("my-addon-1", function messageListener() {
+ Services.ppmm.removeMessageListener("my-addon-1", messageListener);
+ ok(true, "first version sent frame script message");
+ run_next_test();
+ });
+});
+
+// Update to a second version and verify that content gets updated
+add_test(function() {
+ AddonManager.getInstallForURL(TESTROOT + "addons/browser_update1_2.xpi",
+ function(aInstall) {
+ aInstall.install();
+ }, "application/x-xpinstall");
+
+ Services.ppmm.addMessageListener("my-addon-2", function messageListener() {
+ Services.ppmm.removeMessageListener("my-addon-2", messageListener);
+ ok(true, "second version sent frame script message");
+ run_next_test();
+ });
+});
+
+// Finally, cleanup things
+add_test(function() {
+ Services.prefs.setBoolPref("xpinstall.signatures.required", true);
+
+ AddonManager.getAddonByID("update1@tests.mozilla.org", function(aAddon) {
+ aAddon.uninstall();
+
+ finish();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updateid.js b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
new file mode 100644
index 000000000..0fe3eeec5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updateid.js
@@ -0,0 +1,84 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that updates that change an add-on's ID show up correctly in the UI
+
+var gProvider;
+var gManagerWindow;
+var gCategoryUtilities;
+
+var gApp = document.getElementById("bundle_brand").getString("brandShortName");
+
+function test() {
+ waitForExplicitFinish();
+
+ gProvider = new MockProvider();
+
+ gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "manually updating addon",
+ version: "1.0",
+ applyBackgroundUpdates: AddonManager.AUTOUPDATE_DISABLE
+ }]);
+
+ open_manager("addons://list/extension", function(aWindow) {
+ gManagerWindow = aWindow;
+ gCategoryUtilities = new CategoryUtilities(gManagerWindow);
+ run_next_test();
+ });
+}
+
+function end_test() {
+ close_manager(gManagerWindow, function() {
+ finish();
+ });
+}
+
+add_test(function() {
+ gCategoryUtilities.openType("extension", function() {
+ gProvider.createInstalls([{
+ name: "updated add-on",
+ existingAddon: gProvider.addons[0],
+ version: "2.0"
+ }]);
+ var newAddon = new MockAddon("addon2@tests.mozilla.org");
+ newAddon.name = "updated add-on";
+ newAddon.version = "2.0";
+ newAddon.pendingOperations = AddonManager.PENDING_INSTALL;
+ gProvider.installs[0]._addonToInstall = newAddon;
+
+ var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ var name = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "name");
+ is(name.value, "manually updating addon", "Should show the old name in the list");
+ get_tooltip_info(item).then(({ name, version }) => {
+ is(name, "manually updating addon", "Should show the old name in the tooltip");
+ is(version, "1.0", "Should still show the old version in the tooltip");
+
+ var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
+ is_element_visible(update, "Update button should be visible");
+
+ item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+ is(item, null, "Should not show the new version in the list");
+
+ run_next_test();
+ });
+ });
+});
+
+add_test(function() {
+ var item = get_addon_element(gManagerWindow, "addon1@tests.mozilla.org");
+ var update = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "update-btn");
+ EventUtils.synthesizeMouseAtCenter(update, { }, gManagerWindow);
+
+ var pending = gManagerWindow.document.getAnonymousElementByAttribute(item, "anonid", "pending");
+ is_element_visible(pending, "Pending message should be visible");
+ is(pending.textContent,
+ get_string("notification.upgrade", "manually updating addon", gApp),
+ "Pending message should be correct");
+
+ item = get_addon_element(gManagerWindow, "addon2@tests.mozilla.org");
+ is(item, null, "Should not show the new version in the list");
+
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.js b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
new file mode 100644
index 000000000..8b816c927
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.js
@@ -0,0 +1,370 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var tempScope = {};
+Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm", tempScope);
+var AddonUpdateChecker = tempScope.AddonUpdateChecker;
+
+const updaterdf = RELATIVE_DIR + "browser_updatessl.rdf";
+const redirect = RELATIVE_DIR + "redirect.sjs?";
+const SUCCESS = 0;
+const DOWNLOAD_ERROR = AddonUpdateChecker.ERROR_DOWNLOAD_ERROR;
+
+const HTTP = "http://example.com/";
+const HTTPS = "https://example.com/";
+const NOCERT = "https://nocert.example.com/";
+const SELFSIGNED = "https://self-signed.example.com/";
+const UNTRUSTED = "https://untrusted.example.com/";
+const EXPIRED = "https://expired.example.com/";
+
+const PREF_UPDATE_REQUIREBUILTINCERTS = "extensions.update.requireBuiltInCerts";
+
+var gTests = [];
+var gStart = 0;
+var gLast = 0;
+
+var HTTPObserver = {
+ observeActivity: function(aChannel, aType, aSubtype, aTimestamp, aSizeData,
+ aStringData) {
+ aChannel.QueryInterface(Ci.nsIChannel);
+
+ dump("*** HTTP Activity 0x" + aType.toString(16) + " 0x" + aSubtype.toString(16) +
+ " " + aChannel.URI.spec + "\n");
+ }
+};
+
+function test() {
+ gStart = Date.now();
+ requestLongerTimeout(4);
+ waitForExplicitFinish();
+
+ let observerService = Cc["@mozilla.org/network/http-activity-distributor;1"].
+ getService(Ci.nsIHttpActivityDistributor);
+ observerService.addObserver(HTTPObserver);
+
+ registerCleanupFunction(function() {
+ observerService.removeObserver(HTTPObserver);
+ });
+
+ run_next_test();
+}
+
+function end_test() {
+ Services.prefs.clearUserPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+
+ var cos = Cc["@mozilla.org/security/certoverride;1"].
+ getService(Ci.nsICertOverrideService);
+ cos.clearValidityOverride("nocert.example.com", -1);
+ cos.clearValidityOverride("self-signed.example.com", -1);
+ cos.clearValidityOverride("untrusted.example.com", -1);
+ cos.clearValidityOverride("expired.example.com", -1);
+
+ info("All tests completed in " + (Date.now() - gStart) + "ms");
+ finish();
+}
+
+function add_update_test(mainURL, redirectURL, expectedStatus) {
+ gTests.push([mainURL, redirectURL, expectedStatus]);
+}
+
+function run_update_tests(callback) {
+ function run_next_update_test() {
+ if (gTests.length == 0) {
+ callback();
+ return;
+ }
+ gLast = Date.now();
+
+ let [mainURL, redirectURL, expectedStatus] = gTests.shift();
+ if (redirectURL) {
+ var url = mainURL + redirect + redirectURL + updaterdf;
+ var message = "Should have seen the right result for an update check redirected from " +
+ mainURL + " to " + redirectURL;
+ }
+ else {
+ url = mainURL + updaterdf;
+ message = "Should have seen the right result for an update check from " +
+ mainURL;
+ }
+
+ AddonUpdateChecker.checkForUpdates("addon1@tests.mozilla.org",
+ null, url, {
+ onUpdateCheckComplete: function(updates) {
+ is(updates.length, 1, "Should be the right number of results");
+ is(SUCCESS, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ },
+
+ onUpdateCheckError: function(status) {
+ is(status, expectedStatus, message);
+ info("Update test ran in " + (Date.now() - gLast) + "ms");
+ run_next_update_test();
+ }
+ });
+ }
+
+ run_next_update_test();
+}
+
+// Add overrides for the bad certificates
+function addCertOverrides() {
+ addCertOverride("nocert.example.com", Ci.nsICertOverrideService.ERROR_MISMATCH);
+ addCertOverride("self-signed.example.com", Ci.nsICertOverrideService.ERROR_UNTRUSTED);
+ addCertOverride("untrusted.example.com", Ci.nsICertOverrideService.ERROR_UNTRUSTED);
+ addCertOverride("expired.example.com", Ci.nsICertOverrideService.ERROR_TIME);
+}
+
+// Runs tests with built-in certificates required and no certificate exceptions.
+add_test(function() {
+ // Tests that a simple update.rdf retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and no certificate
+// exceptions.
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, false);
+
+ // Tests that a simple update.rdf retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTP, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTP, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests with built-in certificates required and all certificate exceptions.
+add_test(function() {
+ Services.prefs.clearUserPref(PREF_UPDATE_REQUIREBUILTINCERTS);
+ addCertOverrides();
+
+ // Tests that a simple update.rdf retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, null, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, null, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, null, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, null, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, EXPIRED, DOWNLOAD_ERROR);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, NOCERT, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, SELFSIGNED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, UNTRUSTED, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, EXPIRED, DOWNLOAD_ERROR);
+
+ run_update_tests(run_next_test);
+});
+
+// Runs tests without requiring built-in certificates and all certificate
+// exceptions.
+add_test(function() {
+ Services.prefs.setBoolPref(PREF_UPDATE_REQUIREBUILTINCERTS, false);
+
+ // Tests that a simple update.rdf retrieval works as expected.
+ add_update_test(HTTP, null, SUCCESS);
+ add_update_test(HTTPS, null, SUCCESS);
+ add_update_test(NOCERT, null, SUCCESS);
+ add_update_test(SELFSIGNED, null, SUCCESS);
+ add_update_test(UNTRUSTED, null, SUCCESS);
+ add_update_test(EXPIRED, null, SUCCESS);
+
+ // Tests that redirecting from http to other servers works as expected
+ add_update_test(HTTP, HTTP, SUCCESS);
+ add_update_test(HTTP, HTTPS, SUCCESS);
+ add_update_test(HTTP, NOCERT, SUCCESS);
+ add_update_test(HTTP, SELFSIGNED, SUCCESS);
+ add_update_test(HTTP, UNTRUSTED, SUCCESS);
+ add_update_test(HTTP, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from valid https to other servers works as expected
+ add_update_test(HTTPS, HTTP, DOWNLOAD_ERROR);
+ add_update_test(HTTPS, HTTPS, SUCCESS);
+ add_update_test(HTTPS, NOCERT, SUCCESS);
+ add_update_test(HTTPS, SELFSIGNED, SUCCESS);
+ add_update_test(HTTPS, UNTRUSTED, SUCCESS);
+ add_update_test(HTTPS, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from nocert https to other servers works as expected
+ add_update_test(NOCERT, HTTP, DOWNLOAD_ERROR);
+ add_update_test(NOCERT, HTTPS, SUCCESS);
+ add_update_test(NOCERT, NOCERT, SUCCESS);
+ add_update_test(NOCERT, SELFSIGNED, SUCCESS);
+ add_update_test(NOCERT, UNTRUSTED, SUCCESS);
+ add_update_test(NOCERT, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from self-signed https to other servers works as expected
+ add_update_test(SELFSIGNED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(SELFSIGNED, HTTPS, SUCCESS);
+ add_update_test(SELFSIGNED, NOCERT, SUCCESS);
+ add_update_test(SELFSIGNED, SELFSIGNED, SUCCESS);
+ add_update_test(SELFSIGNED, UNTRUSTED, SUCCESS);
+ add_update_test(SELFSIGNED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from untrusted https to other servers works as expected
+ add_update_test(UNTRUSTED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(UNTRUSTED, HTTPS, SUCCESS);
+ add_update_test(UNTRUSTED, NOCERT, SUCCESS);
+ add_update_test(UNTRUSTED, SELFSIGNED, SUCCESS);
+ add_update_test(UNTRUSTED, UNTRUSTED, SUCCESS);
+ add_update_test(UNTRUSTED, EXPIRED, SUCCESS);
+
+ // Tests that redirecting from expired https to other servers works as expected
+ add_update_test(EXPIRED, HTTP, DOWNLOAD_ERROR);
+ add_update_test(EXPIRED, HTTPS, SUCCESS);
+ add_update_test(EXPIRED, NOCERT, SUCCESS);
+ add_update_test(EXPIRED, SELFSIGNED, SUCCESS);
+ add_update_test(EXPIRED, UNTRUSTED, SUCCESS);
+ add_update_test(EXPIRED, EXPIRED, SUCCESS);
+
+ run_update_tests(run_next_test);
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf b/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf
new file mode 100644
index 000000000..f24573847
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf
@@ -0,0 +1,25 @@
+<?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:extension:addon1@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>20</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf^headers^ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf^headers^
new file mode 100644
index 000000000..2e4f8163b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_updatessl.rdf^headers^
@@ -0,0 +1 @@
+Connection: close
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi.js b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
new file mode 100644
index 000000000..ca8e41aad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function testWithAPI(task) {
+ return function*() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, task);
+ }
+}
+
+let gProvider = new MockProvider();
+
+let addons = gProvider.createAddons([{
+ id: "addon1@tests.mozilla.org",
+ name: "Test add-on 1",
+ version: "2.1",
+ description: "Short description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+}, {
+ id: "addon2@tests.mozilla.org",
+ name: "Test add-on 2",
+ version: "5.3.7ab",
+ description: null,
+ type: "theme",
+ userDisabled: false,
+ isActive: false,
+}, {
+ id: "addon3@tests.mozilla.org",
+ name: "Test add-on 3",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: true,
+ isActive: false,
+}, {
+ id: "addon4@tests.mozilla.org",
+ name: "Test add-on 4",
+ version: "1",
+ description: "Longer description",
+ type: "extension",
+ userDisabled: false,
+ isActive: true,
+}]);
+
+addons[3].permissions &= ~AddonManager.PERM_CAN_UNINSTALL;
+
+function API_getAddonByID(browser, id) {
+ return ContentTask.spawn(browser, id, function*(id) {
+ let addon = yield content.navigator.mozAddonManager.getAddonByID(id);
+
+ // We can't send native objects back so clone its properties.
+ let result = {};
+ for (let prop in addon) {
+ result[prop] = addon[prop];
+ }
+
+ return result;
+ });
+}
+
+add_task(testWithAPI(function*(browser) {
+ function compareObjects(web, real) {
+ for (let prop of Object.keys(web)) {
+ let webVal = web[prop];
+ let realVal = real[prop];
+
+ switch (prop) {
+ case "isEnabled":
+ realVal = !real.userDisabled;
+ break;
+
+ case "canUninstall":
+ realVal = Boolean(real.permissions & AddonManager.PERM_CAN_UNINSTALL);
+ break;
+ }
+
+ // null and undefined don't compare well so stringify them first
+ if (realVal === null || realVal === undefined) {
+ realVal = `${realVal}`;
+ webVal = `${webVal}`;
+ }
+
+ is(webVal, realVal, `Property ${prop} should have the right value in add-on ${real.id}`);
+ }
+ }
+
+ let [a1, a2, a3] = yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"]);
+ let w1 = yield API_getAddonByID(browser, "addon1@tests.mozilla.org");
+ let w2 = yield API_getAddonByID(browser, "addon2@tests.mozilla.org");
+ let w3 = yield API_getAddonByID(browser, "addon3@tests.mozilla.org");
+
+ compareObjects(w1, a1);
+ compareObjects(w2, a2);
+ compareObjects(w3, a3);
+}));
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
new file mode 100644
index 000000000..f4b1dc745
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_access.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function check_frame_availability(browser) {
+ return ContentTask.spawn(browser, null, function*() {
+ let frame = content.document.getElementById("frame");
+ return frame.contentWindow.document.getElementById("result").textContent == "true";
+ });
+}
+
+function check_availability(browser) {
+ return ContentTask.spawn(browser, null, function*() {
+ return content.document.getElementById("result").textContent == "true";
+ });
+}
+
+// Test that initially the API isn't available in the test domain
+add_task(function* test_not_available() {
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+ function* test_not_available(browser) {
+ let available = yield check_availability(browser);
+ ok(!available, "API should not be available.");
+ })
+});
+
+// Test that with testing on the API is available in the test domain
+add_task(function* test_available() {
+ Services.prefs.setBoolPref("extensions.webapi.testing", true);
+
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkavailable.html`,
+ function* test_not_available(browser) {
+ let available = yield check_availability(browser);
+ ok(available, "API should be available.");
+ })
+});
+
+// Test that the API is not available in a bad domain
+add_task(function* test_bad_domain() {
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkavailable.html`,
+ function* test_not_available(browser) {
+ let available = yield check_availability(browser);
+ ok(!available, "API should not be available.");
+ })
+});
+
+// Test that the API is only available in https sites
+add_task(function* test_not_available_http() {
+ yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkavailable.html`,
+ function* test_not_available(browser) {
+ let available = yield check_availability(browser);
+ ok(!available, "API should not be available.");
+ })
+});
+
+// Test that the API is available when in a frame of the test domain
+add_task(function* test_available_framed() {
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT}webapi_checkframed.html`,
+ function* test_available(browser) {
+ let available = yield check_frame_availability(browser);
+ ok(available, "API should be available.");
+ })
+});
+
+// Test that if the external frame is http then the inner frame doesn't have
+// the API
+add_task(function* test_not_available_http_framed() {
+ yield BrowserTestUtils.withNewTab(`${TESTROOT}webapi_checkframed.html`,
+ function* test_not_available(browser) {
+ let available = yield check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ })
+});
+
+// Test that if the external frame is a bad domain then the inner frame doesn't
+// have the API
+add_task(function* test_not_available_framed() {
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checkframed.html`,
+ function* test_not_available(browser) {
+ let available = yield check_frame_availability(browser);
+ ok(!available, "API should not be available.");
+ })
+});
+
+// Test that a window navigated to a bad domain doesn't allow access to the API
+add_task(function* test_navigated_window() {
+ yield BrowserTestUtils.withNewTab(`${SECURE_TESTROOT2}webapi_checknavigatedwindow.html`,
+ function* test_available(browser) {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ yield ContentTask.spawn(browser, null, function*() {
+ yield content.wrappedJSObject.openWindow();
+ });
+
+ // Should be a new tab open
+ let tab = yield tabPromise;
+ let loadPromise = BrowserTestUtils.browserLoaded(gBrowser.getBrowserForTab(tab));
+
+ ContentTask.spawn(browser, null, function*() {
+ content.wrappedJSObject.navigate();
+ });
+
+ yield loadPromise;
+
+ let available = yield ContentTask.spawn(browser, null, function*() {
+ return content.wrappedJSObject.check();
+ });
+
+ ok(!available, "API should not be available.");
+
+ gBrowser.removeTab(tab);
+ })
+});
+
+// Check that if a page is embedded in a chrome content UI that it can still
+// access the API.
+add_task(function* test_chrome_frame() {
+ yield BrowserTestUtils.withNewTab(`${CHROMEROOT}webapi_checkchromeframe.xul`,
+ function* test_available(browser) {
+ let available = yield check_frame_availability(browser);
+ ok(available, "API should be available.");
+ })
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
new file mode 100644
index 000000000..b8049f13c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_addon_listener.js
@@ -0,0 +1,174 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+
+function* getListenerEvents(browser) {
+ let result = yield ContentTask.spawn(browser, null, function*() {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split('\n').map(JSON.parse);
+}
+
+const RESTART_ID = "restart@tests.mozilla.org";
+const RESTART_DISABLED_ID = "restart_disabled@tests.mozilla.org";
+const RESTARTLESS_ID = "restartless@tests.mozilla.org";
+const INSTALL_ID = "install@tests.mozilla.org";
+const CANCEL_ID = "cancel@tests.mozilla.org";
+
+let provider = new MockProvider(false);
+provider.createAddons([
+ {
+ id: RESTART_ID,
+ name: "Add-on that requires restart",
+ },
+ {
+ id: RESTART_DISABLED_ID,
+ name: "Disabled add-on that requires restart",
+ userDisabled: true,
+ },
+ {
+ id: RESTARTLESS_ID,
+ name: "Restartless add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+ {
+ id: CANCEL_ID,
+ name: "Add-on for uninstall cancel",
+ },
+]);
+
+// Test disable of add-on requiring restart
+add_task(function* test_disable() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTART_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ addon.userDisabled = true;
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ let events = yield getListenerEvents(browser);
+
+ // Just a single onDisabling since restart is needed to complete
+ let expected = [
+ {id: RESTART_ID, needsRestart: true, event: "onDisabling"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable event");
+ });
+});
+
+// Test enable of add-on requiring restart
+add_task(function* test_enable() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTART_DISABLED_ID);
+ is(addon.userDisabled, true, "addon is disabled");
+
+ // enable it
+ addon.userDisabled = false;
+ is(addon.userDisabled, false, "addon was enabled successfully");
+
+ let events = yield getListenerEvents(browser);
+
+ // Just a single onEnabling since restart is needed to complete
+ let expected = [
+ {id: RESTART_DISABLED_ID, needsRestart: true, event: "onEnabling"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected enable event");
+ });
+});
+
+// Test enable/disable events for restartless
+add_task(function* test_restartless() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTARTLESS_ID);
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // disable it
+ addon.userDisabled = true;
+ is(addon.userDisabled, true, "addon was disabled successfully");
+
+ // re-enable it
+ addon.userDisabled = false;
+ is(addon.userDisabled, false, "addon was re-enabled successfuly");
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onDisabled"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onEnabled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable/enable events");
+ });
+});
+
+// Test install events
+add_task(function* test_restartless() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = new MockAddon(INSTALL_ID, "installme", null,
+ AddonManager.OP_NEED_RESTART_NONE);
+ let install = new MockInstall(null, null, addon);
+
+ let installPromise = new Promise(resolve => {
+ install.addTestListener({
+ onInstallEnded: resolve,
+ });
+ });
+
+ provider.addInstall(install);
+ install.install();
+
+ yield installPromise;
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: INSTALL_ID, needsRestart: false, event: "onInstalling"},
+ {id: INSTALL_ID, needsRestart: false, event: "onInstalled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected install events");
+ });
+});
+
+// Test uninstall
+add_task(function* test_uninstall() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(RESTARTLESS_ID);
+ isnot(addon, null, "Found add-on for uninstall");
+
+ addon.uninstall();
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalling"},
+ {id: RESTARTLESS_ID, needsRestart: false, event: "onUninstalled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected uninstall events");
+ });
+});
+
+// Test cancel of uninstall.
+add_task(function* test_cancel() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(CANCEL_ID);
+ isnot(addon, null, "Found add-on for cancelling uninstall");
+
+ addon.uninstall();
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: CANCEL_ID, needsRestart: true, event: "onUninstalling"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected uninstalling event");
+
+ addon.cancelUninstall();
+ events = yield getListenerEvents(browser);
+ expected.push({id: CANCEL_ID, needsRestart: false, event: "onOperationCancelled"});
+ Assert.deepEqual(events, expected, "Got expected cancel event");
+ });
+});
+
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
new file mode 100644
index 000000000..27d8029c5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_enable.js
@@ -0,0 +1,62 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_addon_listener.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function* getListenerEvents(browser) {
+ let result = yield ContentTask.spawn(browser, null, function*() {
+ return content.document.getElementById("result").textContent;
+ });
+
+ return result.split('\n').map(JSON.parse);
+}
+
+const ID = "test@tests.mozilla.org";
+
+let provider = new MockProvider(false);
+provider.createAddons([
+ {
+ id: ID,
+ name: "Test add-on",
+ operationsRequiringRestart: AddonManager.OP_NEED_RESTART_NONE,
+ },
+]);
+
+// Test disable and enable from content
+add_task(function* () {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, function*(browser) {
+ let addon = yield promiseAddonByID(ID);
+ isnot(addon, null, "Test addon exists");
+ is(addon.userDisabled, false, "addon is enabled");
+
+ // Disable the addon from content.
+ yield ContentTask.spawn(browser, null, function* () {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => { addon.setEnabled(false); });
+ });
+
+ let events = yield getListenerEvents(browser);
+ let expected = [
+ {id: ID, needsRestart: false, event: "onDisabling"},
+ {id: ID, needsRestart: false, event: "onDisabled"},
+ ];
+ Assert.deepEqual(events, expected, "Got expected disable events");
+
+ // Enable the addon from content.
+ yield ContentTask.spawn(browser, null, function* () {
+ return content.navigator.mozAddonManager
+ .getAddonByID("test@tests.mozilla.org")
+ .then(addon => { addon.setEnabled(true); });
+ });
+
+ events = yield getListenerEvents(browser);
+ expected = expected.concat([
+ {id: ID, needsRestart: false, event: "onEnabling"},
+ {id: ID, needsRestart: false, event: "onEnabled"},
+ ]);
+ Assert.deepEqual(events, expected, "Got expected enable events");
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
new file mode 100644
index 000000000..bc31c2331
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_install.js
@@ -0,0 +1,311 @@
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+const XPI_URL = `${SECURE_TESTROOT}addons/browser_webapi_install.xpi`;
+const XPI_SHA = "sha256:d4bab17ff9ba5f635e97c84021f4c527c502250d62ab7f6e6c9e8ee28822f772";
+
+const ID = "webapi_install@tests.mozilla.org";
+// eh, would be good to just stat the real file instead of this...
+const XPI_LEN = 4782;
+
+function waitForClear() {
+ const MSG = "WebAPICleanup";
+ return new Promise(resolve => {
+ let listener = {
+ receiveMessage: function(msg) {
+ if (msg.name == MSG) {
+ Services.mm.removeMessageListener(MSG, listener);
+ resolve();
+ }
+ }
+ };
+
+ Services.mm.addMessageListener(MSG, listener, true);
+ });
+}
+
+add_task(function* setup() {
+ yield SpecialPowers.pushPrefEnv({
+ set: [["extensions.webapi.testing", true],
+ ["extensions.install.requireBuiltInCerts", false]],
+ });
+ info("added preferences");
+});
+
+// Wrapper around a common task to run in the content process to test
+// the mozAddonManager API. Takes a URL for the XPI to install and an
+// array of steps, each of which can either be an action to take
+// (i.e., start or cancel the install) or an install event to wait for.
+// Steps that look for a specific event may also include a "props" property
+// with properties that the AddonInstall object is expected to have when
+// that event is triggered.
+function* testInstall(browser, args, steps, description) {
+ let success = yield ContentTask.spawn(browser, {args, steps}, function* (opts) {
+ let { args, steps } = opts;
+ let install = yield content.navigator.mozAddonManager.createInstall(args);
+ if (!install) {
+ yield Promise.reject("createInstall() did not return an install object");
+ }
+
+ // Check that the initial state of the AddonInstall is sane.
+ if (install.state != "STATE_AVAILABLE") {
+ yield Promise.reject("new install should be in STATE_AVAILABLE");
+ }
+ if (install.error != null) {
+ yield Promise.reject("new install should have null error");
+ }
+
+ const events = [
+ "onDownloadStarted",
+ "onDownloadProgress",
+ "onDownloadEnded",
+ "onDownloadCancelled",
+ "onDownloadFailed",
+ "onInstallStarted",
+ "onInstallEnded",
+ "onInstallCancelled",
+ "onInstallFailed",
+ ];
+ let eventWaiter = null;
+ let receivedEvents = [];
+ let prevEvent = null;
+ events.forEach(event => {
+ install.addEventListener(event, e => {
+ receivedEvents.push({
+ event,
+ state: install.state,
+ error: install.error,
+ progress: install.progress,
+ maxProgress: install.maxProgress,
+ });
+ if (eventWaiter) {
+ eventWaiter();
+ }
+ });
+ });
+
+ // Returns a promise that is resolved when the given event occurs
+ // or rejects if a different event comes first or if props is supplied
+ // and properties on the AddonInstall don't match those in props.
+ function expectEvent(event, props) {
+ return new Promise((resolve, reject) => {
+ function check() {
+ let received = receivedEvents.shift();
+ // Skip any repeated onDownloadProgress events.
+ while (received &&
+ received.event == prevEvent &&
+ prevEvent == "onDownloadProgress") {
+ received = receivedEvents.shift();
+ }
+ // Wait for more events if we skipped all there were.
+ if (!received) {
+ eventWaiter = () => {
+ eventWaiter = null;
+ check();
+ }
+ return;
+ }
+ prevEvent = received.event;
+ if (received.event != event) {
+ let err = new Error(`expected ${event} but got ${received.event}`);
+ reject(err);
+ }
+ if (props) {
+ for (let key of Object.keys(props)) {
+ if (received[key] != props[key]) {
+ throw new Error(`AddonInstall property ${key} was ${received[key]} but expected ${props[key]}`);
+ }
+ }
+ }
+ resolve();
+ }
+ check();
+ });
+ }
+
+ while (steps.length > 0) {
+ let nextStep = steps.shift();
+ if (nextStep.action) {
+ if (nextStep.action == "install") {
+ yield install.install();
+ } else if (nextStep.action == "cancel") {
+ yield install.cancel();
+ } else {
+ throw new Error(`unknown action ${nextStep.action}`);
+ }
+ } else {
+ yield expectEvent(nextStep.event, nextStep.props);
+ }
+ }
+
+ return true;
+ });
+
+ is(success, true, description);
+}
+
+function makeInstallTest(task) {
+ return function*() {
+ // withNewTab() will close the test tab before returning, at which point
+ // the cleanup event will come from the content process. We need to see
+ // that event but don't want to race to install a listener for it after
+ // the tab is closed. So set up the listener now but don't yield the
+ // listening promise until below.
+ let clearPromise = waitForClear();
+
+ yield BrowserTestUtils.withNewTab(TESTPAGE, task);
+
+ yield clearPromise;
+ is(AddonManager.webAPI.installs.size, 0, "AddonInstall was cleaned up");
+ };
+}
+
+function makeRegularTest(options, what) {
+ return makeInstallTest(function* (browser) {
+ let steps = [
+ {action: "install"},
+ {
+ event: "onDownloadStarted",
+ props: {state: "STATE_DOWNLOADING"},
+ },
+ {
+ event: "onDownloadProgress",
+ props: {maxProgress: XPI_LEN},
+ },
+ {
+ event: "onDownloadEnded",
+ props: {
+ state: "STATE_DOWNLOADED",
+ progress: XPI_LEN,
+ maxProgress: XPI_LEN,
+ },
+ },
+ {
+ event: "onInstallStarted",
+ props: {state: "STATE_INSTALLING"},
+ },
+ {
+ event: "onInstallEnded",
+ props: {state: "STATE_INSTALLED"},
+ },
+ ];
+
+ yield testInstall(browser, options, steps, what);
+
+ let version = Services.prefs.getIntPref("webapitest.active_version");
+ is(version, 1, "the install really did work");
+
+ // Sanity check to ensure that the test in makeInstallTest() that
+ // installs.size == 0 means we actually did clean up.
+ ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
+
+ let addons = yield promiseAddonsByIDs([ID]);
+ isnot(addons[0], null, "Found the addon");
+
+ yield addons[0].uninstall();
+
+ addons = yield promiseAddonsByIDs([ID]);
+ is(addons[0], null, "Addon was uninstalled");
+ });
+}
+
+add_task(makeRegularTest({url: XPI_URL}, "a basic install works"));
+add_task(makeRegularTest({url: XPI_URL, hash: null}, "install with hash=null works"));
+add_task(makeRegularTest({url: XPI_URL, hash: ""}, "install with empty string for hash works"));
+add_task(makeRegularTest({url: XPI_URL, hash: XPI_SHA}, "install with hash works"));
+
+add_task(makeInstallTest(function* (browser) {
+ let steps = [
+ {action: "cancel"},
+ {
+ event: "onDownloadCancelled",
+ props: {
+ state: "STATE_CANCELLED",
+ error: null,
+ },
+ }
+ ];
+
+ yield testInstall(browser, {url: XPI_URL}, steps, "canceling an install works");
+
+ let addons = yield promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
+}));
+
+add_task(makeInstallTest(function* (browser) {
+ let steps = [
+ {action: "install"},
+ {
+ event: "onDownloadStarted",
+ props: {state: "STATE_DOWNLOADING"},
+ },
+ {event: "onDownloadProgress"},
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_NETWORK_FAILURE",
+ },
+ }
+ ];
+
+ yield testInstall(browser, {url: XPI_URL + "bogus"}, steps, "install of a bad url fails");
+
+ let addons = yield promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
+}));
+
+add_task(makeInstallTest(function* (browser) {
+ let steps = [
+ {action: "install"},
+ {
+ event: "onDownloadStarted",
+ props: {state: "STATE_DOWNLOADING"},
+ },
+ {event: "onDownloadProgress"},
+ {
+ event: "onDownloadFailed",
+ props: {
+ state: "STATE_DOWNLOAD_FAILED",
+ error: "ERROR_INCORRECT_HASH",
+ },
+ }
+ ];
+
+ yield testInstall(browser, {url: XPI_URL, hash: "sha256:bogus"}, steps, "install with bad hash fails");
+
+ let addons = yield promiseAddonsByIDs([ID]);
+ is(addons[0], null, "The addon was not installed");
+
+ ok(AddonManager.webAPI.installs.size > 0, "webAPI is tracking the AddonInstall");
+}));
+
+add_task(function* test_permissions() {
+ function testBadUrl(url, pattern, successMessage) {
+ return BrowserTestUtils.withNewTab(TESTPAGE, function* (browser) {
+ let result = yield ContentTask.spawn(browser, {url, pattern}, function (opts) {
+ return new Promise(resolve => {
+ content.navigator.mozAddonManager.createInstall({url: opts.url})
+ .then(() => {
+ resolve({success: false, message: "createInstall should not have succeeded"});
+ }, err => {
+ if (err.message.match(new RegExp(opts.pattern))) {
+ resolve({success: true});
+ }
+ resolve({success: false, message: `Wrong error message: ${err.message}`});
+ });
+ });
+ });
+ is(result.success, true, result.message || successMessage);
+ });
+ }
+
+ yield testBadUrl("i am not a url", "NS_ERROR_MALFORMED_URI",
+ "Installing from an unparseable URL fails");
+
+ yield testBadUrl("https://addons.not-really-mozilla.org/impostor.xpi",
+ "not permitted",
+ "Installing from non-approved URL fails");
+});
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
new file mode 100644
index 000000000..724f6fefe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webapi_uninstall.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const TESTPAGE = `${SECURE_TESTROOT}webapi_checkavailable.html`;
+
+Services.prefs.setBoolPref("extensions.webapi.testing", true);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref("extensions.webapi.testing");
+});
+
+function testWithAPI(task) {
+ return function*() {
+ yield BrowserTestUtils.withNewTab(TESTPAGE, task);
+ }
+}
+
+function API_uninstallByID(browser, id) {
+ return ContentTask.spawn(browser, id, function*(id) {
+ let addon = yield content.navigator.mozAddonManager.getAddonByID(id);
+
+ let result = yield addon.uninstall();
+ return result;
+ });
+}
+
+add_task(testWithAPI(function*(browser) {
+ const ID1 = "addon1@tests.mozilla.org";
+ const ID2 = "addon2@tests.mozilla.org";
+
+ let provider = new MockProvider();
+
+ provider.addAddon(new MockAddon(ID1, "Test add-on 1", "extension", 0));
+ provider.addAddon(new MockAddon(ID2, "Test add-on 2", "extension", 0));
+
+ let [a1, a2] = yield promiseAddonsByIDs([ID1, ID2]);
+ isnot(a1, null, "addon1 is installed");
+ isnot(a2, null, "addon2 is installed");
+
+ let result = yield API_uninstallByID(browser, ID1);
+ is(result, true, "uninstall of addon1 succeeded");
+
+ [a1, a2] = yield promiseAddonsByIDs([ID1, ID2]);
+ is(a1, null, "addon1 is uninstalled");
+ isnot(a2, null, "addon2 is still installed");
+
+ result = yield API_uninstallByID(browser, ID2);
+ is(result, true, "uninstall of addon2 succeeded");
+ [a2] = yield promiseAddonsByIDs([ID2]);
+ is(a2, null, "addon2 is uninstalled");
+}));
diff --git a/toolkit/mozapps/extensions/test/browser/browser_webext_options.js b/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
new file mode 100644
index 000000000..b13213f0b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/browser_webext_options.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Wrapper to run a test that consists of:
+// 1. opening the add-ons manager viewing the list of extensions
+// 2. installing an extension (using the provider installer callable)
+// 3. opening the preferences panel for the new extension and verifying
+// that it opens cleanly
+function* runTest(installer) {
+ let mgrWindow = yield open_manager("addons://list/extension");
+
+ let {addon, id} = yield* installer();
+ isnot(addon, null, "Extension is installed");
+
+ let element = get_addon_element(mgrWindow, id);
+ element.parentNode.ensureElementIsVisible(element);
+
+ let button = mgrWindow.document.getAnonymousElementByAttribute(element, "anonid", "preferences-btn");
+ is_element_visible(button, "Preferences button should be visible");
+
+ EventUtils.synthesizeMouseAtCenter(button, {clickCount: 1}, mgrWindow);
+
+ yield TestUtils.topicObserved(AddonManager.OPTIONS_NOTIFICATION_DISPLAYED,
+ (subject, data) => data == id);
+
+ is(mgrWindow.gViewController.currentViewId,
+ `addons://detail/${encodeURIComponent(id)}/preferences`,
+ "Current view should scroll to preferences");
+
+ var browser = mgrWindow.document.querySelector("#detail-grid > rows > .inline-options-browser");
+ var rows = browser.parentNode;
+
+ ok(browser, "Grid should have a browser child");
+ is(browser.localName, "browser", "Grid should have a browser child");
+ is(browser.currentURI.spec, element.mAddon.optionsURL, "Browser has the expected options URL loaded")
+
+ is(browser.clientWidth, rows.clientWidth,
+ "Browser should be the same width as its parent node");
+
+ button = mgrWindow.document.getElementById("detail-prefs-btn");
+ is_element_hidden(button, "Preferences button should not be visible");
+
+ yield close_manager(mgrWindow);
+
+ addon.uninstall();
+}
+
+// Test that deferred handling of optionsURL works for a signed webextension
+add_task(function* test_options_signed() {
+ yield* runTest(function*() {
+ // The extension in-tree is signed with this ID:
+ const ID = "{9792932b-32b2-4567-998c-e7bf6c4c5e35}";
+
+ yield install_addon("addons/options_signed.xpi");
+ let addon = yield promiseAddonByID(ID);
+
+ return {addon, id: ID};
+ });
+});
+
+add_task(function* test_options_temporary() {
+ yield* runTest(function*() {
+ let dir = get_addon_file_url("options_signed").file;
+ let addon = yield AddonManager.installTemporaryAddon(dir);
+ isnot(addon, null, "Extension is installed (temporarily)");
+
+ return {addon, id: addon.id};
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs b/toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs
new file mode 100644
index 000000000..38bc25d08
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/cancelCompatCheck.sjs
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Delay before responding to an HTTP call attempting to read
+// an addon update RDF file
+
+function handleRequest(req, resp) {
+ resp.processAsync();
+ resp.setHeader("Cache-Control", "no-cache, no-store", false);
+ resp.setHeader("Content-Type", "text/xml;charset=utf-8", false);
+
+ let file = null;
+ getObjectState("SERVER_ROOT", function(serverRoot)
+ {
+ file = serverRoot.getFile("browser/toolkit/mozapps/extensions/test/browser/browser_bug557956.rdf");
+ });
+ dump("*** cancelCompatCheck.sjs: " + file.path + "\n");
+ let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ fstream.init(file, -1, 0, 0);
+ let cstream = null;
+ cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ // The delay can be passed on the query string
+ let delay = req.queryString + 0;
+
+ timer = Components.classes["@mozilla.org/timer;1"].
+ createInstance(Components.interfaces.nsITimer);
+ timer.init(function sendFile() {
+ dump("cancelCompatCheck: starting to send file\n");
+ let str = {};
+ let read = 0;
+ do {
+ // read as much as we can and put it in str.value
+ read = cstream.readString(0xffffffff, str);
+ resp.write(str.value);
+ } while (read != 0);
+ cstream.close();
+ resp.finish();
+ }, delay, Components.interfaces.nsITimer.TYPE_ONE_SHOT);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/discovery.html b/toolkit/mozapps/extensions/test/browser/discovery.html
new file mode 100644
index 000000000..72f4fe374
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery.html
@@ -0,0 +1,10 @@
+<html>
+<body>
+ <h1>Test page for the discovery pane</h1>
+ <p><a id="link-normal" href="https://example.com/browser/toolkit/mozapps/extensions/test/browser/discovery.html">Load normal</a></p>
+ <p><a id="link-http" href="http://example.com/browser/toolkit/mozapps/extensions/test/browser/discovery.html">Load insecure</a></p>
+ <p><a id="link-domain" href="https://test1.example.com/browser/toolkit/mozapps/extensions/test/browser/discovery.html">Load other domain</a></p>
+ <p><a id="link-bad" href="https://example.com/browser/toolkit/mozapps/extensions/test/browser/foo.html">Load missing page</a></p>
+ <p><a id="link-good" href="https://example.com/browser/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml">Load other page</a></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/discovery_frame.html b/toolkit/mozapps/extensions/test/browser/discovery_frame.html
new file mode 100644
index 000000000..8ec722812
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery_frame.html
@@ -0,0 +1,6 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<iframe id="frame" width="100%" height="100%" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/discovery_install.html"></iframe>
+</body>
diff --git a/toolkit/mozapps/extensions/test/browser/discovery_install.html b/toolkit/mozapps/extensions/test/browser/discovery_install.html
new file mode 100644
index 000000000..c6499ec03
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/discovery_install.html
@@ -0,0 +1,19 @@
+<html>
+<head>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function install() {
+ InstallTrigger.install({
+ "Test Add-on": {
+ URL: "https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi"
+ }
+ });
+}
+</script>
+</head>
+<body>
+ <h1>Test page for the discovery pane</h1>
+ <p><a id="install-direct" href="https://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi">Direct install</a></p>
+ <p><a id="install-js" href="javascript:install()">JS install</a></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/head.js b/toolkit/mozapps/extensions/test/browser/head.js
new file mode 100644
index 000000000..5a749099d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/head.js
@@ -0,0 +1,1468 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+/* globals end_test*/
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+var tmp = {};
+Components.utils.import("resource://gre/modules/AddonManager.jsm", tmp);
+Components.utils.import("resource://gre/modules/Log.jsm", tmp);
+var AddonManager = tmp.AddonManager;
+var AddonManagerPrivate = tmp.AddonManagerPrivate;
+var Log = tmp.Log;
+
+var pathParts = gTestPath.split("/");
+// Drop the test filename
+pathParts.splice(pathParts.length - 1, pathParts.length);
+
+var gTestInWindow = /-window$/.test(pathParts[pathParts.length - 1]);
+
+// Drop the UI type
+if (gTestInWindow) {
+ pathParts.splice(pathParts.length - 1, pathParts.length);
+}
+
+const RELATIVE_DIR = pathParts.slice(4).join("/") + "/";
+
+const TESTROOT = "http://example.com/" + RELATIVE_DIR;
+const SECURE_TESTROOT = "https://example.com/" + RELATIVE_DIR;
+const TESTROOT2 = "http://example.org/" + RELATIVE_DIR;
+const SECURE_TESTROOT2 = "https://example.org/" + RELATIVE_DIR;
+const CHROMEROOT = pathParts.join("/") + "/";
+const PREF_DISCOVERURL = "extensions.webservice.discoverURL";
+const PREF_DISCOVER_ENABLED = "extensions.getAddons.showPane";
+const PREF_XPI_ENABLED = "xpinstall.enabled";
+const PREF_UPDATEURL = "extensions.update.url";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI = "xpinstall.customConfirmationUI";
+const PREF_UI_LASTCATEGORY = "extensions.ui.lastCategory";
+
+const MANAGER_URI = "about:addons";
+const INSTALL_URI = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const PREF_SEARCH_MAXRESULTS = "extensions.getAddons.maxResults";
+const PREF_STRICT_COMPAT = "extensions.strictCompatibility";
+
+var PREF_CHECK_COMPATIBILITY;
+(function() {
+ var channel = "default";
+ try {
+ channel = Services.prefs.getCharPref("app.update.channel");
+ } catch (e) { }
+ if (channel != "aurora" &&
+ channel != "beta" &&
+ channel != "release" &&
+ channel != "esr") {
+ var version = "nightly";
+ } else {
+ version = Services.appinfo.version.replace(/^([^\.]+\.[0-9]+[a-z]*).*/gi, "$1");
+ }
+ PREF_CHECK_COMPATIBILITY = "extensions.checkCompatibility." + version;
+})();
+
+var gPendingTests = [];
+var gTestsRun = 0;
+var gTestStart = null;
+
+var gUseInContentUI = !gTestInWindow && ("switchToTabHavingURI" in window);
+
+var gRestorePrefs = [{name: PREF_LOGGING_ENABLED},
+ {name: PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI},
+ {name: "extensions.webservice.discoverURL"},
+ {name: "extensions.update.url"},
+ {name: "extensions.update.background.url"},
+ {name: "extensions.update.enabled"},
+ {name: "extensions.update.autoUpdateDefault"},
+ {name: "extensions.getAddons.get.url"},
+ {name: "extensions.getAddons.getWithPerformance.url"},
+ {name: "extensions.getAddons.search.browseURL"},
+ {name: "extensions.getAddons.search.url"},
+ {name: "extensions.getAddons.cache.enabled"},
+ {name: "devtools.chrome.enabled"},
+ {name: PREF_SEARCH_MAXRESULTS},
+ {name: PREF_STRICT_COMPAT},
+ {name: PREF_CHECK_COMPATIBILITY}];
+
+for (let pref of gRestorePrefs) {
+ if (!Services.prefs.prefHasUserValue(pref.name)) {
+ pref.type = "clear";
+ continue;
+ }
+ pref.type = Services.prefs.getPrefType(pref.name);
+ if (pref.type == Services.prefs.PREF_BOOL)
+ pref.value = Services.prefs.getBoolPref(pref.name);
+ else if (pref.type == Services.prefs.PREF_INT)
+ pref.value = Services.prefs.getIntPref(pref.name);
+ else if (pref.type == Services.prefs.PREF_STRING)
+ pref.value = Services.prefs.getCharPref(pref.name);
+}
+
+// Turn logging on for all tests
+Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
+
+Services.prefs.setBoolPref(PREF_CUSTOM_XPINSTALL_CONFIRMATION_UI, false);
+
+// Helper to register test failures and close windows if any are left open
+function checkOpenWindows(aWindowID) {
+ let windows = Services.wm.getEnumerator(aWindowID);
+ let found = false;
+ while (windows.hasMoreElements()) {
+ let win = windows.getNext().QueryInterface(Ci.nsIDOMWindow);
+ if (!win.closed) {
+ found = true;
+ win.close();
+ }
+ }
+ if (found)
+ ok(false, "Found unexpected " + aWindowID + " window still open");
+}
+
+// Tools to disable and re-enable the background update and blocklist timers
+// so that tests can protect themselves from unwanted timer events.
+var gCatMan = Components.classes["@mozilla.org/categorymanager;1"]
+ .getService(Components.interfaces.nsICategoryManager);
+// Default values from toolkit/mozapps/extensions/extensions.manifest, but disable*UpdateTimer()
+// records the actual value so we can put it back in enable*UpdateTimer()
+var backgroundUpdateConfig = "@mozilla.org/addons/integration;1,getService,addon-background-update-timer,extensions.update.interval,86400";
+var blocklistUpdateConfig = "@mozilla.org/extensions/blocklist;1,getService,blocklist-background-update-timer,extensions.blocklist.interval,86400";
+
+var UTIMER = "update-timer";
+var AMANAGER = "addonManager";
+var BLOCKLIST = "nsBlocklistService";
+
+function disableBackgroundUpdateTimer() {
+ info("Disabling " + UTIMER + " " + AMANAGER);
+ backgroundUpdateConfig = gCatMan.getCategoryEntry(UTIMER, AMANAGER);
+ gCatMan.deleteCategoryEntry(UTIMER, AMANAGER, true);
+}
+
+function enableBackgroundUpdateTimer() {
+ info("Enabling " + UTIMER + " " + AMANAGER);
+ gCatMan.addCategoryEntry(UTIMER, AMANAGER, backgroundUpdateConfig, false, true);
+}
+
+function disableBlocklistUpdateTimer() {
+ info("Disabling " + UTIMER + " " + BLOCKLIST);
+ blocklistUpdateConfig = gCatMan.getCategoryEntry(UTIMER, BLOCKLIST);
+ gCatMan.deleteCategoryEntry(UTIMER, BLOCKLIST, true);
+}
+
+function enableBlocklistUpdateTimer() {
+ info("Enabling " + UTIMER + " " + BLOCKLIST);
+ gCatMan.addCategoryEntry(UTIMER, BLOCKLIST, blocklistUpdateConfig, false, true);
+}
+
+registerCleanupFunction(function() {
+ // Restore prefs
+ for (let pref of gRestorePrefs) {
+ if (pref.type == "clear")
+ Services.prefs.clearUserPref(pref.name);
+ else if (pref.type == Services.prefs.PREF_BOOL)
+ Services.prefs.setBoolPref(pref.name, pref.value);
+ else if (pref.type == Services.prefs.PREF_INT)
+ Services.prefs.setIntPref(pref.name, pref.value);
+ else if (pref.type == Services.prefs.PREF_STRING)
+ Services.prefs.setCharPref(pref.name, pref.value);
+ }
+
+ // Throw an error if the add-ons manager window is open anywhere
+ checkOpenWindows("Addons:Manager");
+ checkOpenWindows("Addons:Compatibility");
+ checkOpenWindows("Addons:Install");
+
+ return new Promise((resolve, reject) => AddonManager.getAllInstalls(resolve))
+ .then(aInstalls => {
+ for (let install of aInstalls) {
+ if (install instanceof MockInstall)
+ continue;
+
+ ok(false, "Should not have seen an install of " + install.sourceURI.spec + " in state " + install.state);
+ install.cancel();
+ }
+ });
+});
+
+function log_exceptions(aCallback, ...aArgs) {
+ try {
+ return aCallback.apply(null, aArgs);
+ }
+ catch (e) {
+ info("Exception thrown: " + e);
+ throw e;
+ }
+}
+
+function log_callback(aPromise, aCallback) {
+ aPromise.then(aCallback)
+ .then(null, e => info("Exception thrown: " + e));
+ return aPromise;
+}
+
+function add_test(test) {
+ gPendingTests.push(test);
+}
+
+function run_next_test() {
+ // Make sure we're not calling run_next_test from inside an add_task() test
+ // We're inside the browser_test.js 'testScope' here
+ if (this.__tasks) {
+ throw new Error("run_next_test() called from an add_task() test function. " +
+ "run_next_test() should not be called from inside add_task() " +
+ "under any circumstances!");
+ }
+ if (gTestsRun > 0)
+ info("Test " + gTestsRun + " took " + (Date.now() - gTestStart) + "ms");
+
+ if (gPendingTests.length == 0) {
+ executeSoon(end_test);
+ return;
+ }
+
+ gTestsRun++;
+ var test = gPendingTests.shift();
+ if (test.name)
+ info("Running test " + gTestsRun + " (" + test.name + ")");
+ else
+ info("Running test " + gTestsRun);
+
+ gTestStart = Date.now();
+ executeSoon(() => log_exceptions(test));
+}
+
+var get_tooltip_info = Task.async(function*(addon) {
+ let managerWindow = addon.ownerDocument.defaultView;
+
+ // The popup code uses a triggering event's target to set the
+ // document.tooltipNode property.
+ let nameNode = addon.ownerDocument.getAnonymousElementByAttribute(addon, "anonid", "name");
+ let event = new managerWindow.CustomEvent("TriggerEvent");
+ nameNode.dispatchEvent(event);
+
+ let tooltip = managerWindow.document.getElementById("addonitem-tooltip");
+
+ let promise = BrowserTestUtils.waitForEvent(tooltip, "popupshown");
+ tooltip.openPopup(nameNode, "after_start", 0, 0, false, false, event);
+ yield promise;
+
+ let tiptext = tooltip.label;
+
+ promise = BrowserTestUtils.waitForEvent(tooltip, "popuphidden");
+ tooltip.hidePopup();
+ yield promise;
+
+ let expectedName = addon.getAttribute("name");
+ ok(tiptext.substring(0, expectedName.length), expectedName,
+ "Tooltip should always start with the expected name");
+
+ if (expectedName.length == tiptext.length) {
+ return {
+ name: tiptext,
+ version: undefined
+ };
+ }
+ return {
+ name: tiptext.substring(0, expectedName.length),
+ version: tiptext.substring(expectedName.length + 1)
+ };
+});
+
+function get_addon_file_url(aFilename) {
+ try {
+ var cr = Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(Ci.nsIChromeRegistry);
+ var fileurl = cr.convertChromeURL(makeURI(CHROMEROOT + "addons/" + aFilename));
+ return fileurl.QueryInterface(Ci.nsIFileURL);
+ } catch (ex) {
+ var jar = getJar(CHROMEROOT + "addons/" + aFilename);
+ var tmpDir = extractJarToTmp(jar);
+ tmpDir.append(aFilename);
+
+ return Services.io.newFileURI(tmpDir).QueryInterface(Ci.nsIFileURL);
+ }
+}
+
+function get_current_view(aManager) {
+ let view = aManager.document.getElementById("view-port").selectedPanel;
+ if (view.id == "headered-views") {
+ view = aManager.document.getElementById("headered-views-content").selectedPanel;
+ }
+ is(view, aManager.gViewController.displayedView, "view controller is tracking the displayed view correctly");
+ return view;
+}
+
+function get_test_items_in_list(aManager) {
+ var tests = "@tests.mozilla.org";
+
+ let view = get_current_view(aManager);
+ let listid = view.id == "search-view" ? "search-list" : "addon-list";
+ let item = aManager.document.getElementById(listid).firstChild;
+ let items = [];
+
+ while (item) {
+ if (item.localName != "richlistitem") {
+ item = item.nextSibling;
+ continue;
+ }
+
+ if (!item.mAddon || item.mAddon.id.substring(item.mAddon.id.length - tests.length) == tests)
+ items.push(item);
+ item = item.nextSibling;
+ }
+
+ return items;
+}
+
+function check_all_in_list(aManager, aIds, aIgnoreExtras) {
+ var doc = aManager.document;
+ var view = get_current_view(aManager);
+ var listid = view.id == "search-view" ? "search-list" : "addon-list";
+ var list = doc.getElementById(listid);
+
+ var inlist = [];
+ var node = list.firstChild;
+ while (node) {
+ if (node.value)
+ inlist.push(node.value);
+ node = node.nextSibling;
+ }
+
+ for (let id of aIds) {
+ if (inlist.indexOf(id) == -1)
+ ok(false, "Should find " + id + " in the list");
+ }
+
+ if (aIgnoreExtras)
+ return;
+
+ for (let inlistItem of inlist) {
+ if (aIds.indexOf(inlistItem) == -1)
+ ok(false, "Shouldn't have seen " + inlistItem + " in the list");
+ }
+}
+
+function get_addon_element(aManager, aId) {
+ var doc = aManager.document;
+ var view = get_current_view(aManager);
+ var listid = "addon-list";
+ if (view.id == "search-view")
+ listid = "search-list";
+ else if (view.id == "updates-view")
+ listid = "updates-list";
+ var list = doc.getElementById(listid);
+
+ var node = list.firstChild;
+ while (node) {
+ if (node.value == aId)
+ return node;
+ node = node.nextSibling;
+ }
+ return null;
+}
+
+function wait_for_view_load(aManagerWindow, aCallback, aForceWait, aLongerTimeout) {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ if (!aForceWait && !aManagerWindow.gViewController.isLoading) {
+ log_exceptions(aCallback, aManagerWindow);
+ return;
+ }
+
+ aManagerWindow.document.addEventListener("ViewChanged", function() {
+ aManagerWindow.document.removeEventListener("ViewChanged", arguments.callee, false);
+ log_exceptions(aCallback, aManagerWindow);
+ }, false);
+}
+
+function wait_for_manager_load(aManagerWindow, aCallback) {
+ if (!aManagerWindow.gIsInitializing) {
+ log_exceptions(aCallback, aManagerWindow);
+ return;
+ }
+
+ info("Waiting for initialization");
+ aManagerWindow.document.addEventListener("Initialized", function() {
+ aManagerWindow.document.removeEventListener("Initialized", arguments.callee, false);
+ log_exceptions(aCallback, aManagerWindow);
+ }, false);
+}
+
+function open_manager(aView, aCallback, aLoadCallback, aLongerTimeout) {
+ let p = new Promise((resolve, reject) => {
+
+ function setup_manager(aManagerWindow) {
+ if (aLoadCallback)
+ log_exceptions(aLoadCallback, aManagerWindow);
+
+ if (aView)
+ aManagerWindow.loadView(aView);
+
+ ok(aManagerWindow != null, "Should have an add-ons manager window");
+ is(aManagerWindow.location, MANAGER_URI, "Should be displaying the correct UI");
+
+ waitForFocus(function() {
+ info("window has focus, waiting for manager load");
+ wait_for_manager_load(aManagerWindow, function() {
+ info("Manager waiting for view load");
+ wait_for_view_load(aManagerWindow, function() {
+ resolve(aManagerWindow);
+ }, null, aLongerTimeout);
+ });
+ }, aManagerWindow);
+ }
+
+ if (gUseInContentUI) {
+ info("Loading manager window in tab");
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ if (aSubject.location.href != MANAGER_URI) {
+ info("Ignoring load event for " + aSubject.location.href);
+ return;
+ }
+ setup_manager(aSubject);
+ }, "EM-loaded", false);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ switchToTabHavingURI(MANAGER_URI, true);
+ } else {
+ info("Loading manager window in dialog");
+ Services.obs.addObserver(function (aSubject, aTopic, aData) {
+ Services.obs.removeObserver(arguments.callee, aTopic);
+ setup_manager(aSubject);
+ }, "EM-loaded", false);
+
+ openDialog(MANAGER_URI);
+ }
+ });
+
+ // The promise resolves with the manager window, so it is passed to the callback
+ return log_callback(p, aCallback);
+}
+
+function close_manager(aManagerWindow, aCallback, aLongerTimeout) {
+ let p = new Promise((resolve, reject) => {
+ requestLongerTimeout(aLongerTimeout ? aLongerTimeout : 2);
+
+ ok(aManagerWindow != null, "Should have an add-ons manager window to close");
+ is(aManagerWindow.location, MANAGER_URI, "Should be closing window with correct URI");
+
+ aManagerWindow.addEventListener("unload", function() {
+ try {
+ dump("Manager window unload handler\n");
+ this.removeEventListener("unload", arguments.callee, false);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }, false);
+ });
+
+ info("Telling manager window to close");
+ aManagerWindow.close();
+ info("Manager window close() call returned");
+
+ return log_callback(p, aCallback);
+}
+
+function restart_manager(aManagerWindow, aView, aCallback, aLoadCallback) {
+ if (!aManagerWindow) {
+ return open_manager(aView, aCallback, aLoadCallback);
+ }
+
+ return close_manager(aManagerWindow)
+ .then(() => open_manager(aView, aCallback, aLoadCallback));
+}
+
+function wait_for_window_open(aCallback) {
+ Services.wm.addListener({
+ onOpenWindow: function(aWindow) {
+ Services.wm.removeListener(this);
+
+ let domwindow = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindow);
+ domwindow.addEventListener("load", function() {
+ domwindow.removeEventListener("load", arguments.callee, false);
+ executeSoon(function() {
+ aCallback(domwindow);
+ });
+ }, false);
+ },
+
+ onCloseWindow: function(aWindow) {
+ },
+
+ onWindowTitleChange: function(aWindow, aTitle) {
+ }
+ });
+}
+
+function get_string(aName, ...aArgs) {
+ var bundle = Services.strings.createBundle("chrome://mozapps/locale/extensions/extensions.properties");
+ if (aArgs.length == 0)
+ return bundle.GetStringFromName(aName);
+ return bundle.formatStringFromName(aName, aArgs, aArgs.length);
+}
+
+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 is_hidden(aElement) {
+ var style = aElement.ownerDocument.defaultView.getComputedStyle(aElement, "");
+ if (style.display == "none")
+ return true;
+ if (style.visibility != "visible")
+ return true;
+
+ // Hiding a parent element will hide all its children
+ if (aElement.parentNode != aElement.ownerDocument)
+ return is_hidden(aElement.parentNode);
+
+ return false;
+}
+
+function is_element_visible(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(!is_hidden(aElement), aMsg || (aElement + " should be visible"));
+}
+
+function is_element_hidden(aElement, aMsg) {
+ isnot(aElement, null, "Element should not be null, when checking visibility");
+ ok(is_hidden(aElement), aMsg || (aElement + " should be hidden"));
+}
+
+function promiseAddonByID(aId) {
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(aId, resolve);
+ });
+}
+
+function promiseAddonsByIDs(aIDs) {
+ return new Promise(resolve => {
+ AddonManager.getAddonsByIDs(aIDs, resolve);
+ });
+}
+/**
+ * Install an add-on and call a callback when complete.
+ *
+ * The callback will receive the Addon for the installed add-on.
+ */
+function install_addon(path, cb, pathPrefix=TESTROOT) {
+ let p = new Promise((resolve, reject) => {
+ AddonManager.getInstallForURL(pathPrefix + path, (install) => {
+ install.addListener({
+ onInstallEnded: () => resolve(install.addon),
+ });
+
+ install.install();
+ }, "application/x-xpinstall");
+ });
+
+ return log_callback(p, cb);
+}
+
+function CategoryUtilities(aManagerWindow) {
+ this.window = aManagerWindow;
+
+ var self = this;
+ this.window.addEventListener("unload", function() {
+ self.window.removeEventListener("unload", arguments.callee, false);
+ self.window = null;
+ }, false);
+}
+
+CategoryUtilities.prototype = {
+ window: null,
+
+ get selectedCategory() {
+ isnot(this.window, null, "Should not get selected category when manager window is not loaded");
+ var selectedItem = this.window.document.getElementById("categories").selectedItem;
+ isnot(selectedItem, null, "A category should be selected");
+ var view = this.window.gViewController.parseViewId(selectedItem.value);
+ return (view.type == "list") ? view.param : view.type;
+ },
+
+ get: function(aCategoryType, aAllowMissing) {
+ isnot(this.window, null, "Should not get category when manager window is not loaded");
+ var categories = this.window.document.getElementById("categories");
+
+ var viewId = "addons://list/" + aCategoryType;
+ var items = categories.getElementsByAttribute("value", viewId);
+ if (items.length)
+ return items[0];
+
+ viewId = "addons://" + aCategoryType + "/";
+ items = categories.getElementsByAttribute("value", viewId);
+ if (items.length)
+ return items[0];
+
+ if (!aAllowMissing)
+ ok(false, "Should have found a category with type " + aCategoryType);
+ return null;
+ },
+
+ getViewId: function(aCategoryType) {
+ isnot(this.window, null, "Should not get view id when manager window is not loaded");
+ return this.get(aCategoryType).value;
+ },
+
+ isVisible: function(aCategory) {
+ isnot(this.window, null, "Should not check visible state when manager window is not loaded");
+ if (aCategory.hasAttribute("disabled") &&
+ aCategory.getAttribute("disabled") == "true")
+ return false;
+
+ return !is_hidden(aCategory);
+ },
+
+ isTypeVisible: function(aCategoryType) {
+ return this.isVisible(this.get(aCategoryType));
+ },
+
+ open: function(aCategory, aCallback) {
+
+ isnot(this.window, null, "Should not open category when manager window is not loaded");
+ ok(this.isVisible(aCategory), "Category should be visible if attempting to open it");
+
+ EventUtils.synthesizeMouse(aCategory, 2, 2, { }, this.window);
+ let p = new Promise((resolve, reject) => wait_for_view_load(this.window, resolve));
+
+ return log_callback(p, aCallback);
+ },
+
+ openType: function(aCategoryType, aCallback) {
+ return this.open(this.get(aCategoryType), aCallback);
+ }
+}
+
+function CertOverrideListener(host, bits) {
+ this.host = host;
+ this.bits = bits;
+}
+
+CertOverrideListener.prototype = {
+ host: null,
+ bits: null,
+
+ getInterface: function (aIID) {
+ return this.QueryInterface(aIID);
+ },
+
+ QueryInterface: function(aIID) {
+ if (aIID.equals(Ci.nsIBadCertListener2) ||
+ aIID.equals(Ci.nsIInterfaceRequestor) ||
+ aIID.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.Exception("No interface", Components.results.NS_ERROR_NO_INTERFACE);
+ },
+
+ notifyCertProblem: function (socketInfo, sslStatus, targetHost) {
+ var cert = sslStatus.QueryInterface(Components.interfaces.nsISSLStatus)
+ .serverCert;
+ var cos = Cc["@mozilla.org/security/certoverride;1"].
+ getService(Ci.nsICertOverrideService);
+ cos.rememberValidityOverride(this.host, -1, cert, this.bits, false);
+ return true;
+ }
+}
+
+// Add overrides for the bad certificates
+function addCertOverride(host, bits) {
+ var req = new XMLHttpRequest();
+ try {
+ req.open("GET", "https://" + host + "/", false);
+ req.channel.notificationCallbacks = new CertOverrideListener(host, bits);
+ req.send(null);
+ }
+ catch (e) {
+ // This request will fail since the SSL server is not trusted yet
+ }
+}
+
+/** *** Mock Provider *****/
+
+function MockProvider(aUseAsyncCallbacks, aTypes) {
+ this.addons = [];
+ this.installs = [];
+ this.callbackTimers = [];
+ this.timerLocations = new Map();
+ this.useAsyncCallbacks = (aUseAsyncCallbacks === undefined) ? true : aUseAsyncCallbacks;
+ this.types = (aTypes === undefined) ? [{
+ id: "extension",
+ name: "Extensions",
+ uiPriority: 4000,
+ flags: AddonManager.TYPE_UI_VIEW_LIST |
+ AddonManager.TYPE_SUPPORTS_UNDO_RESTARTLESS_UNINSTALL,
+ }] : aTypes;
+
+ var self = this;
+ registerCleanupFunction(function() {
+ if (self.started)
+ self.unregister();
+ });
+
+ this.register();
+}
+
+MockProvider.prototype = {
+ addons: null,
+ installs: null,
+ started: null,
+ apiDelay: 10,
+ callbackTimers: null,
+ timerLocations: null,
+ useAsyncCallbacks: null,
+ types: null,
+
+ /** *** Utility functions *****/
+
+ /**
+ * Register this provider with the AddonManager
+ */
+ register: function MP_register() {
+ info("Registering mock add-on provider");
+ AddonManagerPrivate.registerProvider(this, this.types);
+ },
+
+ /**
+ * Unregister this provider with the AddonManager
+ */
+ unregister: function MP_unregister() {
+ info("Unregistering mock add-on provider");
+ AddonManagerPrivate.unregisterProvider(this);
+ },
+
+ /**
+ * Adds an add-on to the list of add-ons that this provider exposes to the
+ * AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ addAddon: function MP_addAddon(aAddon) {
+ var oldAddons = this.addons.filter(aOldAddon => aOldAddon.id == aAddon.id);
+ var oldAddon = oldAddons.length > 0 ? oldAddons[0] : null;
+
+ this.addons = this.addons.filter(aOldAddon => aOldAddon.id != aAddon.id);
+
+ this.addons.push(aAddon);
+ aAddon._provider = this;
+
+ if (!this.started)
+ return;
+
+ let requiresRestart = (aAddon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL) != 0;
+ AddonManagerPrivate.callInstallListeners("onExternalInstall", null, aAddon,
+ oldAddon, requiresRestart)
+ },
+
+ /**
+ * Removes an add-on from the list of add-ons that this provider exposes to
+ * the AddonManager, dispatching the onUninstalled event in the process.
+ *
+ * @param aAddon
+ * The add-on to add
+ */
+ removeAddon: function MP_removeAddon(aAddon) {
+ var pos = this.addons.indexOf(aAddon);
+ if (pos == -1) {
+ ok(false, "Tried to remove an add-on that wasn't registered with the mock provider");
+ return;
+ }
+
+ this.addons.splice(pos, 1);
+
+ if (!this.started)
+ return;
+
+ AddonManagerPrivate.callAddonListeners("onUninstalled", aAddon);
+ },
+
+ /**
+ * Adds an add-on install to the list of installs that this provider exposes
+ * to the AddonManager, dispatching appropriate events in the process.
+ *
+ * @param aInstall
+ * The add-on install to add
+ */
+ addInstall: function MP_addInstall(aInstall) {
+ this.installs.push(aInstall);
+ aInstall._provider = this;
+
+ if (!this.started)
+ return;
+
+ aInstall.callListeners("onNewInstall");
+ },
+
+ removeInstall: function MP_removeInstall(aInstall) {
+ var pos = this.installs.indexOf(aInstall);
+ if (pos == -1) {
+ ok(false, "Tried to remove an install that wasn't registered with the mock provider");
+ return;
+ }
+
+ this.installs.splice(pos, 1);
+ },
+
+ /**
+ * Creates a set of mock add-on objects and adds them to the list of add-ons
+ * managed by this provider.
+ *
+ * @param aAddonProperties
+ * An array of objects containing properties describing the add-ons
+ * @return Array of the new MockAddons
+ */
+ createAddons: function MP_createAddons(aAddonProperties) {
+ var newAddons = [];
+ for (let addonProp of aAddonProperties) {
+ let addon = new MockAddon(addonProp.id);
+ for (let prop in addonProp) {
+ if (prop == "id")
+ continue;
+ if (prop == "applyBackgroundUpdates") {
+ addon._applyBackgroundUpdates = addonProp[prop];
+ continue;
+ }
+ if (prop == "appDisabled") {
+ addon._appDisabled = addonProp[prop];
+ continue;
+ }
+ addon[prop] = addonProp[prop];
+ }
+ if (!addon.optionsType && !!addon.optionsURL)
+ addon.optionsType = AddonManager.OPTIONS_TYPE_DIALOG;
+
+ // Make sure the active state matches the passed in properties
+ addon.isActive = addon.shouldBeActive;
+
+ this.addAddon(addon);
+ newAddons.push(addon);
+ }
+
+ return newAddons;
+ },
+
+ /**
+ * Creates a set of mock add-on install objects and adds them to the list
+ * of installs managed by this provider.
+ *
+ * @param aInstallProperties
+ * An array of objects containing properties describing the installs
+ * @return Array of the new MockInstalls
+ */
+ createInstalls: function MP_createInstalls(aInstallProperties) {
+ var newInstalls = [];
+ for (let installProp of aInstallProperties) {
+ let install = new MockInstall(installProp.name || null,
+ installProp.type || null,
+ null);
+ for (let prop in installProp) {
+ switch (prop) {
+ case "name":
+ case "type":
+ break;
+ case "sourceURI":
+ install[prop] = NetUtil.newURI(installProp[prop]);
+ break;
+ default:
+ install[prop] = installProp[prop];
+ }
+ }
+ this.addInstall(install);
+ newInstalls.push(install);
+ }
+
+ return newInstalls;
+ },
+
+ /** *** AddonProvider implementation *****/
+
+ /**
+ * Called to initialize the provider.
+ */
+ startup: function MP_startup() {
+ this.started = true;
+ },
+
+ /**
+ * Called when the provider should shutdown.
+ */
+ shutdown: function MP_shutdown() {
+ if (this.callbackTimers.length) {
+ info("MockProvider: pending callbacks at shutdown(): calling immediately");
+ }
+ while (this.callbackTimers.length > 0) {
+ // When we notify the callback timer, it removes itself from our array
+ let timer = this.callbackTimers[0];
+ try {
+ let setAt = this.timerLocations.get(timer);
+ info("Notifying timer set at " + (setAt || "unknown location"));
+ timer.callback.notify(timer);
+ timer.cancel();
+ } catch (e) {
+ info("Timer notify failed: " + e);
+ }
+ }
+ this.callbackTimers = [];
+ this.timerLocations = null;
+
+ this.started = false;
+ },
+
+ /**
+ * 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 MP_getAddon(aId, aCallback) {
+ for (let addon of this.addons) {
+ if (addon.id == aId) {
+ this._delayCallback(aCallback, addon);
+ return;
+ }
+ }
+
+ 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 MP_getAddonsByTypes(aTypes, aCallback) {
+ var addons = this.addons.filter(function(aAddon) {
+ if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1)
+ return false;
+ return true;
+ });
+ this._delayCallback(aCallback, addons);
+ },
+
+ /**
+ * 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 MP_getAddonsWithOperationsByTypes(aTypes, aCallback) {
+ var addons = this.addons.filter(function(aAddon) {
+ if (aTypes && aTypes.length > 0 && aTypes.indexOf(aAddon.type) == -1)
+ return false;
+ return aAddon.pendingOperations != 0;
+ });
+ this._delayCallback(aCallback, addons);
+ },
+
+ /**
+ * 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 MP_getInstallsByTypes(aTypes, aCallback) {
+ var installs = this.installs.filter(function(aInstall) {
+ // Appear to have actually removed cancelled installs from the provider
+ if (aInstall.state == AddonManager.STATE_CANCELLED)
+ return false;
+
+ if (aTypes && aTypes.length > 0 && aTypes.indexOf(aInstall.type) == -1)
+ return false;
+
+ return true;
+ });
+ this._delayCallback(aCallback, installs);
+ },
+
+ /**
+ * 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 MP_addonChanged(aId, aType, aPendingRestart) {
+ // Not implemented
+ },
+
+ /**
+ * Update the appDisabled property for all add-ons.
+ */
+ updateAddonAppDisabledStates: function MP_updateAddonAppDisabledStates() {
+ // Not needed
+ },
+
+ /**
+ * 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 aIconURL
+ * An icon URL for the install
+ * @param aVersion
+ * A version for the install
+ * @param aLoadGroup
+ * An nsILoadGroup to associate requests with
+ * @param aCallback
+ * A callback to pass the AddonInstall to
+ */
+ getInstallForURL: function MP_getInstallForURL(aUrl, aHash, aName, aIconURL,
+ aVersion, aLoadGroup, aCallback) {
+ // Not yet implemented
+ },
+
+ /**
+ * 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 MP_getInstallForFile(aFile, aCallback) {
+ // Not yet implemented
+ },
+
+ /**
+ * Called to test whether installing add-ons is enabled.
+ *
+ * @return true if installing is enabled
+ */
+ isInstallEnabled: function MP_isInstallEnabled() {
+ 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 supported
+ */
+ supportsMimetype: function MP_supportsMimetype(aMimetype) {
+ return false;
+ },
+
+ /**
+ * Called to test whether installing add-ons from a URI is allowed.
+ *
+ * @param aUri
+ * The URI being installed from
+ * @return true if installing is allowed
+ */
+ isInstallAllowed: function MP_isInstallAllowed(aUri) {
+ return false;
+ },
+
+
+ /** *** Internal functions *****/
+
+ /**
+ * Delay calling a callback to fake a time-consuming async operation.
+ * The delay is specified by the apiDelay property, in milliseconds.
+ * Parameters to send to the callback should be specified as arguments after
+ * the aCallback argument.
+ *
+ * @param aCallback Callback to eventually call
+ */
+ _delayCallback: function MP_delayCallback(aCallback, ...aArgs) {
+ if (!this.useAsyncCallbacks) {
+ aCallback(...aArgs);
+ return;
+ }
+
+ let timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ // Need to keep a reference to the timer, so it doesn't get GC'ed
+ this.callbackTimers.push(timer);
+ // Capture a stack trace where the timer was set
+ // needs the 'new Error' hack until bug 1007656
+ this.timerLocations.set(timer, Log.stackTrace(new Error("dummy")));
+ timer.initWithCallback(() => {
+ let idx = this.callbackTimers.indexOf(timer);
+ if (idx == -1) {
+ dump("MockProvider._delayCallback lost track of timer set at "
+ + (this.timerLocations.get(timer) || "unknown location") + "\n");
+ } else {
+ this.callbackTimers.splice(idx, 1);
+ }
+ this.timerLocations.delete(timer);
+ aCallback(...aArgs);
+ }, this.apiDelay, timer.TYPE_ONE_SHOT);
+ }
+};
+
+/** *** Mock Addon object for the Mock Provider *****/
+
+function MockAddon(aId, aName, aType, aOperationsRequiringRestart) {
+ // Only set required attributes.
+ this.id = aId || "";
+ this.name = aName || "";
+ this.type = aType || "extension";
+ this.version = "";
+ this.isCompatible = true;
+ this.providesUpdatesSecurely = true;
+ this.blocklistState = 0;
+ this._appDisabled = false;
+ this._userDisabled = false;
+ this._applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ this.scope = AddonManager.SCOPE_PROFILE;
+ this.isActive = true;
+ this.creator = "";
+ this.pendingOperations = 0;
+ this._permissions = AddonManager.PERM_CAN_UNINSTALL |
+ AddonManager.PERM_CAN_ENABLE |
+ AddonManager.PERM_CAN_DISABLE |
+ AddonManager.PERM_CAN_UPGRADE;
+ this.operationsRequiringRestart = (aOperationsRequiringRestart != undefined) ?
+ aOperationsRequiringRestart :
+ (AddonManager.OP_NEEDS_RESTART_INSTALL |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL |
+ AddonManager.OP_NEEDS_RESTART_ENABLE |
+ AddonManager.OP_NEEDS_RESTART_DISABLE);
+}
+
+MockAddon.prototype = {
+ get isCorrectlySigned() {
+ if (this.signedState === AddonManager.SIGNEDSTATE_NOT_REQUIRED)
+ return true;
+ return this.signedState > AddonManager.SIGNEDSTATE_MISSING;
+ },
+
+ get shouldBeActive() {
+ return !this.appDisabled && !this._userDisabled &&
+ !(this.pendingOperations & AddonManager.PENDING_UNINSTALL);
+ },
+
+ get appDisabled() {
+ return this._appDisabled;
+ },
+
+ set appDisabled(val) {
+ if (val == this._appDisabled)
+ return val;
+
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["appDisabled"]);
+
+ var currentActive = this.shouldBeActive;
+ this._appDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+
+ return val;
+ },
+
+ get userDisabled() {
+ return this._userDisabled;
+ },
+
+ set userDisabled(val) {
+ if (val == this._userDisabled)
+ return val;
+
+ var currentActive = this.shouldBeActive;
+ this._userDisabled = val;
+ var newActive = this.shouldBeActive;
+ this._updateActiveState(currentActive, newActive);
+
+ return val;
+ },
+
+ get permissions() {
+ let permissions = this._permissions;
+ if (this.appDisabled || !this._userDisabled)
+ permissions &= ~AddonManager.PERM_CAN_ENABLE;
+ if (this.appDisabled || this._userDisabled)
+ permissions &= ~AddonManager.PERM_CAN_DISABLE;
+ return permissions;
+ },
+
+ set permissions(val) {
+ return this._permissions = val;
+ },
+
+ get applyBackgroundUpdates() {
+ return this._applyBackgroundUpdates;
+ },
+
+ set applyBackgroundUpdates(val) {
+ if (val != AddonManager.AUTOUPDATE_DEFAULT &&
+ val != AddonManager.AUTOUPDATE_DISABLE &&
+ val != AddonManager.AUTOUPDATE_ENABLE) {
+ ok(false, "addon.applyBackgroundUpdates set to an invalid value: " + val);
+ }
+ this._applyBackgroundUpdates = val;
+ AddonManagerPrivate.callAddonListeners("onPropertyChanged", this, ["applyBackgroundUpdates"]);
+ },
+
+ isCompatibleWith: function(aAppVersion, aPlatformVersion) {
+ return true;
+ },
+
+ findUpdates: function(aListener, aReason, aAppVersion, aPlatformVersion) {
+ // Tests can implement this if they need to
+ },
+
+ uninstall: function(aAlwaysAllowUndo = false) {
+ if ((this.operationsRequiringRestart & AddonManager.OP_NEED_RESTART_UNINSTALL)
+ && this.pendingOperations & AddonManager.PENDING_UNINSTALL)
+ throw Components.Exception("Add-on is already pending uninstall");
+
+ var needsRestart = aAlwaysAllowUndo || !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_UNINSTALL);
+ this.pendingOperations |= AddonManager.PENDING_UNINSTALL;
+ AddonManagerPrivate.callAddonListeners("onUninstalling", this, needsRestart);
+ if (!needsRestart) {
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this._provider.removeAddon(this);
+ } else if (!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE)) {
+ this.isActive = false;
+ }
+ },
+
+ cancelUninstall: function() {
+ if (!(this.pendingOperations & AddonManager.PENDING_UNINSTALL))
+ throw Components.Exception("Add-on is not pending uninstall");
+
+ this.pendingOperations -= AddonManager.PENDING_UNINSTALL;
+ this.isActive = this.shouldBeActive;
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ },
+
+ markAsSeen: function() {
+ this.seen = true;
+ },
+
+ _updateActiveState: function(currentActive, newActive) {
+ if (currentActive == newActive)
+ return;
+
+ if (newActive == this.isActive) {
+ this.pendingOperations -= (newActive ? AddonManager.PENDING_DISABLE : AddonManager.PENDING_ENABLE);
+ AddonManagerPrivate.callAddonListeners("onOperationCancelled", this);
+ }
+ else if (newActive) {
+ let needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_ENABLE);
+ this.pendingOperations |= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_ENABLE;
+ AddonManagerPrivate.callAddonListeners("onEnabled", this);
+ }
+ }
+ else {
+ let needsRestart = !!(this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_DISABLE);
+ this.pendingOperations |= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabling", this, needsRestart);
+ if (!needsRestart) {
+ this.isActive = newActive;
+ this.pendingOperations -= AddonManager.PENDING_DISABLE;
+ AddonManagerPrivate.callAddonListeners("onDisabled", this);
+ }
+ }
+ }
+};
+
+/** *** Mock AddonInstall object for the Mock Provider *****/
+
+function MockInstall(aName, aType, aAddonToInstall) {
+ this.name = aName || "";
+ // Don't expose type until download completed
+ this._type = aType || "extension";
+ this.type = null;
+ this.version = "1.0";
+ this.iconURL = "";
+ this.infoURL = "";
+ this.state = AddonManager.STATE_AVAILABLE;
+ this.error = 0;
+ this.sourceURI = null;
+ this.file = null;
+ this.progress = 0;
+ this.maxProgress = -1;
+ this.certificate = null;
+ this.certName = "";
+ this.existingAddon = null;
+ this.addon = null;
+ this._addonToInstall = aAddonToInstall;
+ this.listeners = [];
+
+ // Another type of install listener for tests that want to check the results
+ // of code run from standard install listeners
+ this.testListeners = [];
+}
+
+MockInstall.prototype = {
+ install: function() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_DOWNLOADING;
+ if (!this.callListeners("onDownloadStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onDownloadCancelled");
+ return;
+ }
+
+ this.type = this._type;
+
+ // Adding addon to MockProvider to be implemented when needed
+ if (this._addonToInstall)
+ this.addon = this._addonToInstall;
+ else {
+ this.addon = new MockAddon("", this.name, this.type);
+ this.addon.version = this.version;
+ this.addon.pendingOperations = AddonManager.PENDING_INSTALL;
+ }
+ this.addon.install = this;
+ if (this.existingAddon) {
+ if (!this.addon.id)
+ this.addon.id = this.existingAddon.id;
+ this.existingAddon.pendingUpgrade = this.addon;
+ this.existingAddon.pendingOperations |= AddonManager.PENDING_UPGRADE;
+ }
+
+ this.state = AddonManager.STATE_DOWNLOADED;
+ this.callListeners("onDownloadEnded");
+
+ case AddonManager.STATE_DOWNLOADED:
+ this.state = AddonManager.STATE_INSTALLING;
+ if (!this.callListeners("onInstallStarted")) {
+ this.state = AddonManager.STATE_CANCELLED;
+ this.callListeners("onInstallCancelled");
+ return;
+ }
+
+ let needsRestart = (this.operationsRequiringRestart & AddonManager.OP_NEEDS_RESTART_INSTALL);
+ AddonManagerPrivate.callAddonListeners("onInstalling", this.addon, needsRestart);
+ if (!needsRestart) {
+ AddonManagerPrivate.callAddonListeners("onInstalled", this.addon);
+ }
+
+ this.state = AddonManager.STATE_INSTALLED;
+ this.callListeners("onInstallEnded");
+ break;
+ case AddonManager.STATE_DOWNLOADING:
+ case AddonManager.STATE_CHECKING:
+ case AddonManager.STATE_INSTALLING:
+ // Installation is already running
+ return;
+ default:
+ ok(false, "Cannot start installing when state = " + this.state);
+ }
+ },
+
+ cancel: function() {
+ switch (this.state) {
+ case AddonManager.STATE_AVAILABLE:
+ this.state = AddonManager.STATE_CANCELLED;
+ break;
+ case AddonManager.STATE_INSTALLED:
+ this.state = AddonManager.STATE_CANCELLED;
+ this._provider.removeInstall(this);
+ this.callListeners("onInstallCancelled");
+ break;
+ default:
+ // Handling cancelling when downloading to be implemented when needed
+ ok(false, "Cannot cancel when state = " + this.state);
+ }
+ },
+
+
+ addListener: function(aListener) {
+ if (!this.listeners.some(i => i == aListener))
+ this.listeners.push(aListener);
+ },
+
+ removeListener: function(aListener) {
+ this.listeners = this.listeners.filter(i => i != aListener);
+ },
+
+ addTestListener: function(aListener) {
+ if (!this.testListeners.some(i => i == aListener))
+ this.testListeners.push(aListener);
+ },
+
+ removeTestListener: function(aListener) {
+ this.testListeners = this.testListeners.filter(i => i != aListener);
+ },
+
+ callListeners: function(aMethod) {
+ var result = AddonManagerPrivate.callInstallListeners(aMethod, this.listeners,
+ this, this.addon);
+
+ // Call test listeners after standard listeners to remove race condition
+ // between standard and test listeners
+ for (let listener of this.testListeners) {
+ try {
+ if (aMethod in listener)
+ if (listener[aMethod].call(listener, this, this.addon) === false)
+ result = false;
+ }
+ catch (e) {
+ ok(false, "Test listener threw exception: " + e);
+ }
+ }
+
+ return result;
+ }
+};
+
+function waitForCondition(condition, nextTest, errorMsg) {
+ let tries = 0;
+ let interval = setInterval(function() {
+ if (tries >= 30) {
+ ok(false, errorMsg);
+ moveOn();
+ }
+ var conditionPassed;
+ try {
+ conditionPassed = condition();
+ } catch (e) {
+ ok(false, e + "\n" + e.stack);
+ conditionPassed = false;
+ }
+ if (conditionPassed) {
+ moveOn();
+ }
+ tries++;
+ }, 100);
+ let moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
+function getTestPluginTag() {
+ let ph = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ let tags = ph.getPluginTags();
+
+ // Find the test plugin
+ for (let i = 0; i < tags.length; i++) {
+ if (tags[i].name == "Test Plug-in")
+ return tags[i];
+ }
+ ok(false, "Unable to find plugin");
+ return null;
+}
diff --git a/toolkit/mozapps/extensions/test/browser/more_options.xul b/toolkit/mozapps/extensions/test/browser/more_options.xul
new file mode 100644
index 000000000..28dbb0a2e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/more_options.xul
@@ -0,0 +1,32 @@
+<?xml version="1.0" ?>
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting pref="extensions.inlinesettings3.radioBool" type="radio" title="Radio">
+ <radiogroup>
+ <radio label="Delta" value="true" />
+ <radio label="Echo" value="false" />
+ </radiogroup>
+ </setting>
+ <setting pref="extensions.inlinesettings3.radioInt" type="radio" title="Radio">
+ <radiogroup>
+ <radio label="Foxtrot" value="4" />
+ <radio label="Golf" value="5" />
+ <radio label="Hotel" value="6" />
+ </radiogroup>
+ </setting>
+ <setting pref="extensions.inlinesettings3.radioString" type="radio" title="Radio">
+ <radiogroup>
+ <radio label="India" value="india" />
+ <radio label="Juliet" value="juliet" />
+ <radio label="Kilo &#x338F;" value="kilo &#x338F;" />
+ </radiogroup>
+ </setting>
+ <setting pref="extensions.inlinesettings3.menulist" type="menulist" title="Menulist">
+ <menulist sizetopopup="always">
+ <menupopup>
+ <menuitem label="Lima" value="7" />
+ <menuitem label="Mike" value="8" />
+ <menuitem label="November" value="9" />
+ </menupopup>
+ </menulist>
+ </setting>
+</vbox>
diff --git a/toolkit/mozapps/extensions/test/browser/moz.build b/toolkit/mozapps/extensions/test/browser/moz.build
new file mode 100644
index 000000000..af04aaeef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/moz.build
@@ -0,0 +1,10 @@
+# -*- 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/.
+
+BROWSER_CHROME_MANIFESTS += [
+ 'browser-window.ini',
+ 'browser.ini',
+]
diff --git a/toolkit/mozapps/extensions/test/browser/options.xul b/toolkit/mozapps/extensions/test/browser/options.xul
new file mode 100644
index 000000000..1b6827915
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/options.xul
@@ -0,0 +1,12 @@
+<?xml version="1.0" ?>
+<vbox xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <setting />
+ <setting pref="extensions.inlinesettings2.bool1" type="bool" title="Bool 1" desc="Description Attribute"/>
+ <setting pref="extensions.inlinesettings2.bool2" type="bool" title="Bool 2">Description Text Node</setting>
+ <setting type="control" title="Button">
+ This is a test, <button label="button" />all this text should be visible
+ </setting>
+ <setting type="unsupported">
+ This setting should never appear
+ </setting>
+</vbox>
diff --git a/toolkit/mozapps/extensions/test/browser/plugin_test.html b/toolkit/mozapps/extensions/test/browser/plugin_test.html
new file mode 100644
index 000000000..0709eda06
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/plugin_test.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+<html>
+<head><meta charset="utf-8"></head>
+<body>
+<object id="test" width=200 height=200 type="application/x-test"></object>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/redirect.sjs b/toolkit/mozapps/extensions/test/browser/redirect.sjs
new file mode 100644
index 000000000..8f9d1c08a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/redirect.sjs
@@ -0,0 +1,5 @@
+function handleRequest(request, response) {
+ dump("*** Received redirect for " + request.queryString + "\n");
+ response.setStatusLine(request.httpVersion, 301, "Moved Permanently");
+ response.setHeader("Location", request.queryString, false);
+}
diff --git a/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml b/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml
new file mode 100644
index 000000000..63ae07901
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/releaseNotes.xhtml
@@ -0,0 +1,15 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
+<html lang="en-US" dir="ltr" xmlns="http://www.w3.org/1999/xhtml">
+<head>
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
+ <title></title>
+</head>
+
+<body>
+ <h1>OMG, an update!!!!</h1>
+ <ul>
+ <li>Made everything more awesome</li>
+ <li>Added hot sauce</li>
+ </ul>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf b/toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf
new file mode 100644
index 000000000..39bd936c9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/signed_hotfix.rdf
@@ -0,0 +1,26 @@
+<?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:extension:hotfix@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi b/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi
new file mode 100644
index 000000000..bd1890573
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/signed_hotfix.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf b/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf
new file mode 100644
index 000000000..2b4ba9362
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.rdf
@@ -0,0 +1,26 @@
+<?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:extension:hotfix@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>https://example.com/browser/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi b/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi
new file mode 100644
index 000000000..f2d475bd2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/unsigned_hotfix.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
new file mode 100644
index 000000000..56128fd9c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_addon_listener.html
@@ -0,0 +1,30 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+let events = [];
+let resultEl = document.getElementById("result");
+[ "onEnabling",
+ "onEnabled",
+ "onDisabling",
+ "onDisabled",
+ "onInstalling",
+ "onInstalled",
+ "onUninstalling",
+ "onUninstalled",
+ "onOperationCancelled",
+].forEach(event => {
+ navigator.mozAddonManager.addEventListener(event, data => {
+ let obj = {event, id: data.id, needsRestart: data.needsRestart};
+ events.push(JSON.stringify(obj));
+ resultEl.textContent = events.join('\n');
+ });
+});
+</script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
new file mode 100644
index 000000000..141f09cc6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+
+<html>
+<head>
+ <meta charset="utf-8">
+</head>
+<body>
+<p id="result"></p>
+<script type="text/javascript">
+document.getElementById("result").textContent = ("mozAddonManager" in window.navigator);
+</script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul
new file mode 100644
index 000000000..76e642604
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkchromeframe.xul
@@ -0,0 +1,6 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/" type="text/css"?>
+<window xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+ <browser id="frame" disablehistory="true" flex="1" type="content"
+ src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html"/>
+</window>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
new file mode 100644
index 000000000..146769978
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checkframed.html
@@ -0,0 +1,7 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<iframe id="frame" height="200" width="200" src="https://example.com/browser/toolkit/mozapps/extensions/test/browser/webapi_checkavailable.html">
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
new file mode 100644
index 000000000..ba3653310
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/browser/webapi_checknavigatedwindow.html
@@ -0,0 +1,28 @@
+<!DOCTYPE html>
+
+<html>
+<body>
+<script type="text/javascript">
+var nav, win;
+
+function openWindow() {
+ return new Promise(resolve => {
+ win = window.open(window.location);
+
+ win.addEventListener("load", function listener() {
+ nav = win.navigator;
+ resolve();
+ }, false);
+ });
+}
+
+function navigate() {
+ win.location = "http://example.com/";
+}
+
+function check() {
+ return "mozAddonManager" in nav;
+}
+</script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/mochitest/.eslintrc.js b/toolkit/mozapps/extensions/test/mochitest/.eslintrc.js
new file mode 100644
index 000000000..383da0f41
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/mochitest/mochitest.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/extensions/test/mochitest/file_bug687194.xpi b/toolkit/mozapps/extensions/test/mochitest/file_bug687194.xpi
new file mode 100644
index 000000000..e215cffad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/file_bug687194.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/mochitest/file_empty.html b/toolkit/mozapps/extensions/test/mochitest/file_empty.html
new file mode 100644
index 000000000..b6c8a53b4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/file_empty.html
@@ -0,0 +1,2 @@
+<!DOCTYPE html>
+<html><head></head><body><span id="text">Nothing to see here</span></body></html>
diff --git a/toolkit/mozapps/extensions/test/mochitest/mochitest.ini b/toolkit/mozapps/extensions/test/mochitest/mochitest.ini
new file mode 100644
index 000000000..b14cfe87a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/mochitest.ini
@@ -0,0 +1,9 @@
+[DEFAULT]
+support-files =
+ file_empty.html
+ file_bug687194.xpi
+
+[test_bug609794.html]
+[test_bug687194.html]
+skip-if = e10s || os == "android" # this test creates its own child process, no need to run it in e10s
+[test_bug887098.html]
diff --git a/toolkit/mozapps/extensions/test/mochitest/test_bug609794.html b/toolkit/mozapps/extensions/test/mochitest/test_bug609794.html
new file mode 100644
index 000000000..d13e6ef2f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/test_bug609794.html
@@ -0,0 +1,27 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=609794
+-->
+<head>
+ <title>Test for Bug 609794</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=609794">Mozilla Bug 609794</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<pre id="test">
+<script type="application/javascript">
+
+/** Test for Bug 609794 **/
+var obj = Object.create(window);
+is(Object.prototype.toString.call(obj.InstallTrigger), "[object InstallTriggerImpl]", "can get InstallTrigger through the prototype");
+
+</script>
+</pre>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/mochitest/test_bug687194.html b/toolkit/mozapps/extensions/test/mochitest/test_bug687194.html
new file mode 100644
index 000000000..8f99ea73a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/test_bug687194.html
@@ -0,0 +1,133 @@
+<!DOCTYPE HTML>
+<html>
+<head>
+ <title>Test for registering/unregistering chrome OOP</title>
+ <script type="application/javascript"
+ src="/tests/SimpleTest/SimpleTest.js">
+ </script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+</head>
+<body>
+
+ <script type="application/javascript;version=1.8">
+ "use strict";
+
+ SimpleTest.waitForExplicitFinish();
+
+ const childFrameURL =
+ "data:text/html,<!DOCTYPE HTML><html><body></body></html>";
+
+ function childFrameScript() {
+ "use strict";
+
+ var ios =
+ Components.classes["@mozilla.org/network/io-service;1"]
+ .getService(Components.interfaces.nsIIOService);
+ let cr =
+ Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Ci.nsIXULChromeRegistry);
+ addMessageListener("test687194:resolveChromeURI", function(message) {
+ let result;
+ let threw = false;
+ try {
+ let uri = ios.newURI(message.data.URI, null, null);
+ result = cr.convertChromeURL(uri).spec;
+ } catch (e) {
+ threw = true;
+ result = "EXCEPTION: " + e;
+ }
+
+ message.target.sendAsyncMessage("test687194:resolveChromeURI:Answer",
+ { threw: threw, result: result });
+ });
+ }
+
+ let test;
+ function* testStructure(mm) {
+ let lastResult;
+
+ mm.addMessageListener("test687194:resolveChromeURI:Answer", function(msg) {
+ test.next(msg.data);
+ });
+
+ mm.sendAsyncMessage("test687194:resolveChromeURI",
+ { URI: "chrome://bug687194/content/e10sbug.js" });
+ lastResult = yield;
+ is(lastResult.threw, true, "URI shouldn't resolve to begin with");
+
+ let { AddonManager } = SpecialPowers.Cu.import("resource://gre/modules/AddonManager.jsm", {});
+ const INSTALL_URI =
+ "http://mochi.test:8888/tests/toolkit/mozapps/extensions/test/mochitest/file_bug687194.xpi"
+ AddonManager.getInstallForURL(INSTALL_URI, (install) => {
+ install = SpecialPowers.wrap(install);
+ install.addListener(SpecialPowers.wrapCallbackObject({
+ onInstallEnded: function(install, addon) {
+ SimpleTest.executeSoon(() => test.next(addon));
+ }
+ }));
+ install.install();
+ }, "application/x-xpinstall");
+
+ let addon = SpecialPowers.wrap(yield);
+
+ mm.sendAsyncMessage("test687194:resolveChromeURI",
+ { URI: "chrome://bug687194/content/e10sbug.js" });
+ lastResult = yield;
+ is(lastResult.threw, false, "able to resolve after the installation");
+
+ let listener = SpecialPowers.wrapCallbackObject({
+ onUninstalled: function(removedAddon) {
+ if (removedAddon.id === addon.id) {
+ AddonManager.removeAddonListener(listener);
+ SimpleTest.executeSoon(() => test.next());
+ }
+ }
+ });
+ AddonManager.addAddonListener(listener);
+ addon.uninstall();
+
+ yield;
+
+ mm.sendAsyncMessage("test687194:resolveChromeURI",
+ { URI: "chrome://bug687194/content/e10sbug.js" });
+ lastResult = yield;
+ is(lastResult.threw, true, "should have unregistered the URI");
+ SimpleTest.finish();
+ }
+
+ function runTests() {
+ info("Browser prefs set.");
+
+ let iframe = document.createElement("iframe");
+ SpecialPowers.wrap(iframe).mozbrowser = true;
+ iframe.id = "iframe";
+ iframe.src = childFrameURL;
+
+ iframe.addEventListener("mozbrowserloadend", function() {
+ info("Got iframe load event.");
+ let mm = SpecialPowers.getBrowserFrameMessageManager(iframe);
+ mm.loadFrameScript("data:,(" + childFrameScript.toString() + ")();",
+ false);
+
+ test = testStructure(mm);
+ test.next();
+ });
+
+ document.body.appendChild(iframe);
+ }
+
+ addEventListener("load", function() {
+ info("Got load event.");
+
+ SpecialPowers.addPermission("browser", true, document);
+ SpecialPowers.pushPrefEnv({
+ "set": [
+ ["dom.ipc.browser_frames.oop_by_default", true],
+ ["dom.mozBrowserFramesEnabled", true],
+ ["browser.pagethumbnails.capturing_disabled", true]
+ ]
+ }, runTests);
+ });
+ </script>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html b/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html
new file mode 100644
index 000000000..535799b27
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/mochitest/test_bug887098.html
@@ -0,0 +1,52 @@
+<!DOCTYPE HTML>
+<html>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=887098
+-->
+<head>
+ <meta charset="utf-8">
+ <title>Test for Bug 887098</title>
+ <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
+ <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
+ <script type="application/javascript">
+
+ /** Test for Bug 887098 **/
+ SimpleTest.waitForExplicitFinish();
+ /* globals $,evalRef */
+
+ function loaded() {
+ var iwin = $('ifr').contentWindow;
+ var href = SpecialPowers.wrap(iwin).location.href;
+ if (/file_empty/.test(href)) {
+ window.evalRef = iwin.eval;
+ window.installTriggerRef = iwin.InstallTrigger; // Force lazy instantiation.
+ // about: is privileged, so we need to be privileged to load it.
+ SpecialPowers.wrap(iwin).location.href = 'about:';
+ } else {
+ is(href, 'about:', "Successfully navigated to about:");
+ try {
+ evalRef('InstallTrigger.install({URL: "chrome://global/skin/global.css"});');
+ ok(false, "Should have thrown when trying to install restricted URI from InstallTrigger");
+ } catch (e) {
+ // XXXgijs this test broke because of the switch to webidl. I'm told
+ // it has to do with compartments and the fact that we eval in "about:".
+ // Tracking in bug 1007671
+ todo(/permission/.test(e), "We should throw a security exception. Got: " + e);
+ }
+ SimpleTest.finish();
+ }
+ }
+
+ </script>
+</head>
+<body>
+<a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=887098">Mozilla Bug 887098</a>
+<p id="display"></p>
+<div id="content" style="display: none">
+
+</div>
+<iframe onload="loaded();" id="ifr" src="file_empty.html"></iframe>
+<pre id="test">
+</pre>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/moz.build b/toolkit/mozapps/extensions/test/moz.build
new file mode 100644
index 000000000..716659198
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/moz.build
@@ -0,0 +1,19 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+DIRS += ['browser']
+
+BROWSER_CHROME_MANIFESTS += ['xpinstall/browser.ini']
+MOCHITEST_MANIFESTS += ['mochitest/mochitest.ini']
+
+TESTING_JS_MODULES += [
+ 'AddonManagerTesting.jsm',
+]
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'xpcshell/xpcshell-unpack.ini',
+ 'xpcshell/xpcshell.ini',
+]
diff --git a/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..ba65517f9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
new file mode 100644
index 000000000..7c1e4aa9d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/BootstrapMonitor.jsm
@@ -0,0 +1,30 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+this.EXPORTED_SYMBOLS = [ "monitor" ];
+
+function notify(event, originalMethod, data, reason) {
+ let info = {
+ event,
+ data: Object.assign({}, data, {
+ installPath: data.installPath.path,
+ resourceURI: data.resourceURI.spec,
+ }),
+ reason
+ };
+
+ let subject = {wrappedJSObject: {data}};
+
+ Services.obs.notifyObservers(subject, "bootstrapmonitor-event", JSON.stringify(info));
+
+ // If the bootstrap scope already declares a method call it
+ if (originalMethod)
+ originalMethod(data, reason);
+}
+
+// Allows a simple one-line bootstrap script:
+// Components.utils.import("resource://xpcshelldata/bootstrapmonitor.jsm").monitor(this);
+this.monitor = function(scope) {
+ for (let event of ["install", "startup", "shutdown", "uninstall"]) {
+ scope[event] = notify.bind(null, event, scope[event]);
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_change.xml b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_change.xml
new file mode 100644
index 000000000..a229a653a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_change.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="softblock1@tests.mozilla.org">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="softblock2@tests.mozilla.org">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="softblock3@tests.mozilla.org">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="softblock4@tests.mozilla.org">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="softblock5@tests.mozilla.org">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="hardblock@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="3"/>
+ </emItem>
+ <!-- Two RegExp matches, so test flags work - first shouldn't match. -->
+ <emItem id="/^RegExp/">
+ <versionRange severity="1" minVersion="2" maxVersion="3"/>
+ </emItem>
+ <emItem id="/^RegExp/i">
+ <versionRange severity="2" minVersion="2" maxVersion="3"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update1.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update1.rdf
new file mode 100644
index 000000000..588290968
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update1.rdf
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <RDF:Description about="urn:mozilla:extension:softblock1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft1_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft2_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft3_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock4@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft4_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:theme:softblock5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft5_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:hardblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_hard1_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:regexpblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_regexp1_2.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update2.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update2.rdf
new file mode 100644
index 000000000..5c3747f5f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update2.rdf
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <RDF:Description about="urn:mozilla:extension:softblock1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft1_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft2_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft3_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock4@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft4_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:theme:softblock5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft5_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:hardblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_hard1_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:regexpblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>3</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_regexp1_3.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update3.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update3.rdf
new file mode 100644
index 000000000..d60708414
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/addon_update3.rdf
@@ -0,0 +1,144 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <RDF:Description about="urn:mozilla:extension:softblock1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft1_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft2_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft3_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:softblock4@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft4_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:theme:softblock5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_soft5_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:hardblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_hard1_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:regexpblock@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>4</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/blocklist_regexp1_1.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/app_update.xml b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/app_update.xml
new file mode 100644
index 000000000..85a66fe55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/app_update.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="softblock1@tests.mozilla.org">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="softblock2@tests.mozilla.org">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="softblock3@tests.mozilla.org">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="softblock4@tests.mozilla.org">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="softblock5@tests.mozilla.org">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="hardblock@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="/^RegExp/">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="/^RegExp/i">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update1.xml
new file mode 100644
index 000000000..87011cd39
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update1.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist"/>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update2.xml
new file mode 100644
index 000000000..867a34255
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/blocklist_update2.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="softblock1@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="softblock2@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="softblock3@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="softblock4@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="softblock5@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="hardblock@tests.mozilla.org"/>
+ <emItem id="/^RegExp/">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="/^RegExp/i"/>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/manual_update.xml b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/manual_update.xml
new file mode 100644
index 000000000..df9276525
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/blocklistchange/manual_update.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="softblock1@tests.mozilla.org">
+ <versionRange severity="1" minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="softblock2@tests.mozilla.org">
+ <versionRange severity="1" minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="softblock3@tests.mozilla.org">
+ <versionRange severity="1" minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="softblock4@tests.mozilla.org">
+ <versionRange severity="1" minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="softblock5@tests.mozilla.org">
+ <versionRange severity="1" minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="hardblock@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2"/>
+ </emItem>
+ <emItem id="/^RegExp/i">
+ <versionRange minVersion="1" maxVersion="2"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml
new file mode 100644
index 000000000..1f673ef2f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_block.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="test_bug455906_1@tests.mozilla.org" blockID="test_bug455906_1@tests.mozilla.org"/>
+ <emItem id="test_bug455906_2@tests.mozilla.org" blockID="test_bug455906_2@tests.mozilla.org"/>
+ <emItem id="test_bug455906_3@tests.mozilla.org" blockID="test_bug455906_3@tests.mozilla.org"/>
+ <emItem id="test_bug455906_4@tests.mozilla.org" blockID="test_bug455906_4@tests.mozilla.org"/>
+ <emItem id="test_bug455906_5@tests.mozilla.org" blockID="test_bug455906_5@tests.mozilla.org"/>
+ <emItem id="test_bug455906_6@tests.mozilla.org" blockID="test_bug455906_6@tests.mozilla.org"/>
+ <emItem id="test_bug455906_7@tests.mozilla.org" blockID="test_bug455906_7@tests.mozilla.org"/>
+ </emItems>
+ <pluginItems>
+ <pluginItem blockID="test_bug455906_plugin">
+ <match name="name" exp="^test_bug455906"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml
new file mode 100644
index 000000000..88d22f281
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_empty.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="dummy_bug455906_2@tests.mozilla.org"/>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml
new file mode 100644
index 000000000..daba6f4c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_start.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="test_bug455906_4@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_5@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ <emItem id="test_bug455906_6@tests.mozilla.org">
+ <versionRange severity="2"/>
+ </emItem>
+ <emItem id="dummy_bug455906_1@tests.mozilla.org"/>
+ </emItems>
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^test_bug455906_4$"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug455906_5$"/>
+ <versionRange severity="1"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug455906_6$"/>
+ <versionRange severity="2"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml
new file mode 100644
index 000000000..232fd0d07
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/bug455906_warn.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="test_bug455906_1@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_2@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_3@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_4@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_5@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_6@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ <emItem id="test_bug455906_7@tests.mozilla.org">
+ <versionRange severity="-1"/>
+ </emItem>
+ </emItems>
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^test_bug455906"/>
+ <versionRange severity="-1"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi
new file mode 100644
index 000000000..35d7bd5e5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/corrupt.xpi
@@ -0,0 +1 @@
+This is a corrupt zip file
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi
new file mode 100644
index 000000000..0c30989aa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/corruptfile.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi
new file mode 100644
index 000000000..74ed2b817
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/empty.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/install.rdf
new file mode 100644
index 000000000..f02a3869c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/from_sources/install.rdf
@@ -0,0 +1,28 @@
+<?xml version="1.0"?>
+
+<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>bootstrap1@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Bootstrap 1</em:name>
+ <em:description>Test Description</em:description>
+
+ <em:iconURL>chrome://foo/skin/icon.png</em:iconURL>
+ <em:aboutURL>chrome://foo/content/about.xul</em:aboutURL>
+ <em:optionsURL>chrome://foo/content/options.xul</em:optionsURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml b/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml
new file mode 100644
index 000000000..75e252a46
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/pluginInfoURL_block.xml
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ </emItems>
+ <pluginItems>
+ <pluginItem blockID="test_plugin_wInfoURL">
+ <match name="name" exp="^test_with_infoURL"/>
+ <match name="version" exp="^5"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="*"/>
+ </targetApplication>
+ </versionRange>
+ <infoURL>http://test.url.com/</infoURL>
+ </pluginItem>
+ <pluginItem blockID="test_plugin_wAltInfoURL">
+ <match name="name" exp="^test_with_altInfoURL"/>
+ <match name="version" exp="^5"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="*"/>
+ </targetApplication>
+ </versionRange>
+ <infoURL>http://alt.test.url.com/</infoURL>
+ </pluginItem>
+ <pluginItem blockID="test_plugin_noInfoURL">
+ <match name="name" exp="^test_no_infoURL"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="*"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem blockID="test_plugin_newVersion">
+ <match name="name" exp="^test_newVersion"/>
+ <infoURL>http://test.url2.com/</infoURL>
+ <versionRange minVersion="1" maxVersion="2">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="*"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt
new file mode 100644
index 000000000..f17f98b15
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.txt
@@ -0,0 +1 @@
+Not an xml file! \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml
new file mode 100644
index 000000000..0e3d415c4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<foobar></barfoo> \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml
new file mode 100644
index 000000000..55ad1c7d5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/bad2.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<test></test>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml
new file mode 100644
index 000000000..42cb20bd0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/empty.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<updates>
+ <addons></addons>
+</updates>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml
new file mode 100644
index 000000000..e1da86fa5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/good.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<updates>
+ <addons>
+ <addon id="test1" URL="http://example.com/test1.xpi"/>
+ <addon id="test2" URL="http://example.com/test2.xpi" hashFunction="md5" hashValue="djhfgsjdhf"/>
+ <addon id="test3" URL="http://example.com/test3.xpi" version="1.0" size="45"/>
+ <addon id="test4"/>
+ <addon URL="http://example.com/test5.xpi"/>
+ </addons>
+</updates>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml
new file mode 100644
index 000000000..8c9501478
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/missing.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<updates></updates>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi
new file mode 100644
index 000000000..51b00475a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/productaddons/unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js
new file mode 100644
index 000000000..0cf01d319
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/bootstrap.js
@@ -0,0 +1,29 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const VERSION = 1;
+
+// Test steps chain from pref observers on *_reason,
+// so always set that last
+function install(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.install_oldversion", data.oldVersion);
+ Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.startup_oldversion", data.oldVersion);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 0);
+ Services.prefs.setIntPref("bootstraptest.shutdown_newversion", data.newVersion);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+ Services.prefs.setIntPref("bootstraptest.uninstall_newversion", data.newVersion);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf
new file mode 100644
index 000000000..9d08c357c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>test@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Add-on</em:name>
+ <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt
new file mode 100644
index 000000000..11686f61c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_1/test.txt
@@ -0,0 +1 @@
+This test file can be altered to break signing checks.
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js
new file mode 100644
index 000000000..718c82be4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/bootstrap.js
@@ -0,0 +1,29 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const VERSION = 2;
+
+// Test steps chain from pref observers on *_reason,
+// so always set that last
+function install(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.install_oldversion", data.oldVersion);
+ Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", VERSION);
+ Services.prefs.setIntPref("bootstraptest.startup_oldversion", data.oldVersion);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 0);
+ Services.prefs.setIntPref("bootstraptest.shutdown_newversion", data.newVersion);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+ Services.prefs.setIntPref("bootstraptest.uninstall_newversion", data.newVersion);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf
new file mode 100644
index 000000000..0a345dd92
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/install.rdf
@@ -0,0 +1,24 @@
+<?xml version="1.0"?>
+
+<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>test@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+ <em:bootstrap>true</em:bootstrap>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Add-on</em:name>
+ <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>4</em:minVersion>
+ <em:maxVersion>6</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt
new file mode 100644
index 000000000..11686f61c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/bootstrap_2/test.txt
@@ -0,0 +1 @@
+This test file can be altered to break signing checks.
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_badid.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_badid.xpi
new file mode 100644
index 000000000..9d6f0c708
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_badid.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_broken.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_broken.xpi
new file mode 100644
index 000000000..4496a90cf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_broken.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_good.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_good.xpi
new file mode 100644
index 000000000..e61e3c721
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/hotfix_good.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_hash.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_hash.xpi
new file mode 100644
index 000000000..1682a7506
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_hash.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_plain.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_plain.xpi
new file mode 100644
index 000000000..cd67e25fc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_63_plain.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_hash.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_hash.xpi
new file mode 100644
index 000000000..e4040a274
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_hash.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_plain.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_plain.xpi
new file mode 100644
index 000000000..ca453b9d5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_64_plain.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_65_hash.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_65_hash.xpi
new file mode 100644
index 000000000..69579d2dc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/long_65_hash.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_badid.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_badid.xpi
new file mode 100644
index 000000000..6e23eb214
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_badid.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_broken.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_broken.xpi
new file mode 100644
index 000000000..0ba0f30d1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_broken.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_signed.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_signed.xpi
new file mode 100644
index 000000000..33101f63c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_signed.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_unsigned.xpi
new file mode 100644
index 000000000..3146870d8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/multi_unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf
new file mode 100644
index 000000000..97ae60988
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>test@tests.mozilla.org</em:id>
+ <em:version>1.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Add-on</em:name>
+ <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt
new file mode 100644
index 000000000..11686f61c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_1/test.txt
@@ -0,0 +1 @@
+This test file can be altered to break signing checks.
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf
new file mode 100644
index 000000000..df2fd8081
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>test@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Test Add-on</em:name>
+ <em:updateURL>http://localhost:4444/update.rdf</em:updateURL>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>4</em:minVersion>
+ <em:maxVersion>6</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt
new file mode 100644
index 000000000..11686f61c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/nonbootstrap_2/test.txt
@@ -0,0 +1 @@
+This test file can be altered to break signing checks.
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/preliminary_bootstrap_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/preliminary_bootstrap_2.xpi
new file mode 100644
index 000000000..ec38fcc65
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/preliminary_bootstrap_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_1.xpi
new file mode 100644
index 000000000..fc2842399
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_2.xpi
new file mode 100644
index 000000000..327c8a187
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_badid_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_badid_2.xpi
new file mode 100644
index 000000000..efad21d1b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_bootstrap_badid_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_2.xpi
new file mode 100644
index 000000000..d6ddbcec3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_badid_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_badid_2.xpi
new file mode 100644
index 000000000..5898d83e4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/signed_nonbootstrap_badid_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_bootstrap_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_bootstrap_2.xpi
new file mode 100644
index 000000000..9d50f0825
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_bootstrap_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_nonbootstrap_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_nonbootstrap_2.xpi
new file mode 100644
index 000000000..6ba1efd72
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/signing_checks/unsigned_nonbootstrap_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1.xpi
new file mode 100644
index 000000000..2fc2fd189
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1_badcert.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1_badcert.xpi
new file mode 100644
index 000000000..e7e50c8ea
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_1_badcert.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_2.xpi
new file mode 100644
index 000000000..a858cf74a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system1_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_1.xpi
new file mode 100644
index 000000000..911632e49
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_2.xpi
new file mode 100644
index 000000000..102a053bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_3.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_3.xpi
new file mode 100644
index 000000000..295e77611
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system2_3.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_1.xpi
new file mode 100644
index 000000000..954995619
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_2.xpi
new file mode 100644
index 000000000..dc8632aef
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_3.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_3.xpi
new file mode 100644
index 000000000..3f818172a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system3_3.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system4_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system4_1.xpi
new file mode 100644
index 000000000..1f70b1a75
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system4_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system5_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system5_1.xpi
new file mode 100644
index 000000000..fc636e97f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system5_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_1_unpack.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_1_unpack.xpi
new file mode 100644
index 000000000..ff620966d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_1_unpack.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_2_notBootstrap.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_2_notBootstrap.xpi
new file mode 100644
index 000000000..e474dbd59
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_2_notBootstrap.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_3_notMultiprocess.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_3_notMultiprocess.xpi
new file mode 100644
index 000000000..1ccde90c5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system6_3_notMultiprocess.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete.xpi
new file mode 100644
index 000000000..94d9e47d2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete_2.xpi
new file mode 100644
index 000000000..28c8561c6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_complete_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer.xpi
new file mode 100644
index 000000000..daf55c0d4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_2.xpi
new file mode 100644
index 000000000..75cacbbc8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also.xpi
new file mode 100644
index 000000000..2eb6b7fc9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also_2.xpi
new file mode 100644
index 000000000..fb588b3e0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_defer_also_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore.xpi
new file mode 100644
index 000000000..7a5eb265d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpi
new file mode 100644
index 000000000..dc6749355
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_delay_ignore_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_failed_update.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_failed_update.xpi
new file mode 100644
index 000000000..3c673ac2e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/system_addons/system_failed_update.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml
new file mode 100644
index 000000000..0bebca2c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository.xml
@@ -0,0 +1,820 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1111">
+ <!-- Passes all requirements -->
+ <addon>
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors>
+ <author>
+ <name>Test Creator 1</name>
+ <link>http://localhost:%PORT%/creator1.html</link>
+ </author>
+ </authors>
+ <status id="8">Preliminarily Reviewed</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <!-- Test that a negative rating is ignored -->
+ <rating>-2</rating>
+ <!-- Test that a <reviews> with a blank review URL is ignored -->
+ <reviews num=" 1111 "> </reviews>
+ <!-- Test that a negative total_downloads is ignored -->
+ <total_downloads>-2</total_downloads>
+ <install>http://localhost:%PORT%/test1.xpi</install>
+ </addon>
+
+ <!-- Passes requirements. Tests optional attributes. Also tests that
+ integer properties that are NaN in the XML are ignored -->
+ <addon>
+ <name>PASS</name>
+ <!-- Test that extensions pass -->
+ <type id="1">Extension</type>
+ <guid>test2@tests.mozilla.org</guid>
+ <version>1.2</version>
+ <authors>
+ <!-- Test that the first author becomes the creator,
+ and the second one is a developer -->
+ <author>
+ <name>Test Creator 2</name>
+ <link>http://localhost:%PORT%/creator2.html</link>
+ </author>
+ <author>
+ <name>Test Developer 2</name>
+ <link>http://localhost:%PORT%/developer2.html</link>
+ </author>
+ </authors>
+ <summary>&lt;h1&gt;Test Summary 2&lt;/h1&gt;&lt;p&gt;paragraph&lt;/p&gt;</summary>
+ <description>Test Description 2&lt;br&gt;newline</description>
+ <developer_comments>Test Developer
+ Comments 2</developer_comments>
+ <eula>Test EULA 2</eula>
+ <icon size="64">http://localhost:%PORT%/icon2-64.png</icon>
+ <icon size="48">http://localhost:%PORT%/icon2-48.png</icon>
+ <icon size="32">http://localhost:%PORT%/icon2-32.png</icon>
+ <status id="4">Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <!-- Test that multiple preview images are correctly parsed -->
+ <previews>
+ <preview primary="0">
+ <full type="image/png">http://localhost:%PORT%/full1-2.png</full>
+ <thumbnail type="image/png">http://localhost:%PORT%/thumbnail1-2.png</thumbnail>
+ </preview>
+ <preview primary="0">
+ <full type="image/png">http://localhost:%PORT%/full2-2.png</full>
+ <thumbnail type="image/png">http://localhost:%PORT%/thumbnail2-2.png</thumbnail>
+ <caption>Caption 2</caption>
+ </preview>
+ </previews>
+ <rating>NaN</rating>
+ <!-- Test that learnmore is used as the add-on's homepageURL
+ if there is no homepage defined -->
+ <learnmore>http://localhost:%PORT%/learnmore2.html</learnmore>
+ <homepage/>
+ <support>http://localhost:%PORT%/support2.html</support>
+ <contribution_data>
+ <link>http://localhost:%PORT%/contribution2.html</link>
+ <meet_developers>http://localhost:%PORT%/meetDevelopers2.html</meet_developers>
+ </contribution_data>
+ <reviews num="NaN">http://localhost:%PORT%/review2.html</reviews>
+ <total_downloads>NaN</total_downloads>
+ <weekly_downloads>NaN</weekly_downloads>
+ <daily_users>NaN</daily_users>
+ <last_updated epoch="NaN">Not an acual date</last_updated>
+ <install size="NaN" os="ALL">http://localhost:%PORT%/test2.xpi</install>
+ </addon>
+
+ <!-- Passes requirements. Tests optional attributes with extra whitespace. -->
+ <addon>
+ <name> PASS </name>
+ <!-- Test that themes pass -->
+ <type id=" 2 ">Theme</type>
+ <guid> test3@tests.mozilla.org </guid>
+ <version> 1.3 </version>
+ <authors>
+ <!-- Test that authors with blank names are ignored -->
+ <author>
+ <name> </name>
+ <link> http://localhost:%PORT%/ignore3.html </link>
+ </author>
+ <!-- Test that authors with blank links are ignored -->
+ <author>
+ <name> Test Creator Ignore </name>
+ <link> </link>
+ </author>
+ <author>
+ <name> Test Creator 3 </name>
+ <link> http://localhost:%PORT%/creator3.html </link>
+ </author>
+ <author>
+ <name> First Test Developer 3 </name>
+ <link> http://localhost:%PORT%/developer1-3.html </link>
+ </author>
+ <author>
+ <name> </name>
+ <link> </link>
+ </author>
+ <author>
+ <name> Second Test Developer 3 </name>
+ <link> http://localhost:%PORT%/developer2-3.html </link>
+ </author>
+ </authors>
+ <summary> Test Summary 3 </summary>
+ <description> Test Description 3&lt;br&gt;&lt;ul&gt;&lt;li&gt;List item 1&lt;li&gt;List item 2&lt;/ul&gt; </description>
+ <developer_comments> Test Developer Comments 3 </developer_comments>
+ <eula> Test EULA 3 </eula>
+ <icon size="32"> http://localhost:%PORT%/icon3.png </icon>
+ <status id=" 8 ">Preliminarily Reviewed</status>
+ <!-- Test that an incompatible + compatible application list passes -->
+ <compatible_applications>
+ <application>
+ <appID> unknown@tests.mozilla.org </appID>
+ <min_version> 1 </min_version>
+ <max_version> 1 </max_version>
+ </application>
+ <application>
+ <appID> xpcshell@tests.mozilla.org </appID>
+ <min_version> 1 </min_version>
+ <max_version> 1 </max_version>
+ </application>
+ </compatible_applications>
+ <!-- Test that primary images appear first in the add-on's screenshots array -->
+ <previews>
+ <preview primary=" 0 ">
+ <full type=" image/png "> http://localhost:%PORT%/full2-3.png </full>
+ <caption> Caption 2 - 3 </caption>
+ </preview>
+ <!-- Test that a preview without a <full> element is ignored -->
+ <preview primary=" 0 ">
+ <caption> Caption ignore - 3 </caption>
+ </preview>
+ <!-- Test that a preview with an empty <full> element is ignored -->
+ <preview primary=" 0 ">
+ <full type=" image/png "> </full>
+ <caption> Caption ignore - 3 </caption>
+ <preview primary=" 1 ">
+ <full type=" image/png "> http://localhost:%PORT%/full1-3.png </full>
+ <thumbnail type=" image/png "> http://localhost:%PORT%/thumbnail1-3.png </thumbnail>
+ <caption> Caption 1 - 3 </caption>
+ </preview>
+ <preview primary=" 0 ">
+ <full type=" image/png "> http://localhost:%PORT%/full3-3.png </full>
+ <thumbnail type=" image/png "> http://localhost:%PORT%/thumbnail3-3.png </thumbnail>
+ <caption> Caption 3 - 3 </caption>
+ </preview>
+ </preview>
+ </previews>
+ <!-- Test that a rating between 1 and 5 is correctly parsed -->
+ <rating> 2 </rating>
+ <!-- Test that hompage is used as the add-on's homepageURL
+ even if learnmore is defined -->
+ <learnmore> http://localhost:%PORT%/learnmore3.html </learnmore>
+ <homepage> http://localhost:%PORT%/homepage3.html </homepage>
+ <support> http://localhost:%PORT%/support3.html </support>
+ <contribution_data>
+ <link> http://localhost:%PORT%/contribution3.html </link>
+ <suggested_amount currency="USD"> $11.11 </suggested_amount>
+ <meet_developers> http://localhost:%PORT%/meetDevelopers3.html </meet_developers>
+ </contribution_data>
+ <reviews num=" 1111 "> http://localhost:%PORT%/review3.html </reviews>
+ <total_downloads> 2222 </total_downloads>
+ <weekly_downloads> 3333 </weekly_downloads>
+ <daily_users> 4444 </daily_users>
+ <last_updated epoch=" 1265033045 "> 2010-02-01T14:04:05Z </last_updated>
+ <!-- Test that an incompatible install is ignored -->
+ <install size=" 9999 " os=" UNKNOWN "> http://localhost:%PORT%/fail3.xpi </install>
+ <!-- Test that OS matching is case-insensitive -->
+ <install size=" 5555 " os=" xpCShell " hash=" sha1:c26f0b0d62e5dcddcda95074d3f3fedb9bbc26e3 "> http://localhost:%PORT%/test3.xpi </install>
+ </addon>
+
+ <!-- Fails because name is undefined -->
+ <addon>
+ <type id="1">Extension</type>
+ <guid>test4@tests.mozilla.org</guid>
+ <version>1.4</version>
+ <authors><author><name>Test Creator 4</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined name should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test4.xpi</install>
+ </addon>
+
+ <!-- Fails because name is empty-->
+ <addon>
+ <name> </name>
+ <type id="1">Extension</type>
+ <guid>test5@tests.mozilla.org</guid>
+ <version>1.5</version>
+ <authors><author><name>Test Creator 5</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with empty name should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test5.xpi</install>
+ </addon>
+
+ <!-- Fails because type is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <guid>test6@tests.mozilla.org</guid>
+ <version>1.6</version>
+ <authors><author><name>Test Creator 6</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined type should be ignored</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test6.xpi</install>
+ </addon>
+
+ <!-- Fails because type is empty -->
+ <addon>
+ <name>FAIL</name>
+ <type id="">Empty id attribute</type>
+ <guid>test7@tests.mozilla.org</guid>
+ <version>1.7</version>
+ <authors><author><name>Test Creator 7</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with empty type should be ignored</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test7.xpi</install>
+ </addon>
+
+ <!-- Fails because type is unknown -->
+ <addon>
+ <name>FAIL</name>
+ <type id="9999">Unknown</type>
+ <guid>test8@tests.mozilla.org</guid>
+ <version>1.8</version>
+ <authors><author><name>Test Creator 8</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with unknown type should be ignored</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test8.xpi</install>
+ </addon>
+
+ <!-- Fails because guid is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <version>1.9</version>
+ <authors><author><name>Test Creator 9</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined guid should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test9.xpi</install>
+ </addon>
+
+ <!-- Fails because guid is empty -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid> </guid>
+ <version>1.10</version>
+ <authors><author><name>Test Creator 10</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with empty guid should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test10.xpi</install>
+ </addon>
+
+ <!-- Fails because guid matches previously successful result -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.11</version>
+ <authors><author><name>Test Creator 11</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with a guid that matches a previously successful result should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test11.xpi</install>
+ </addon>
+
+ <!-- Fails because guid matches already installed add-on -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test_AddonRepository_1@tests.mozilla.org</guid>
+ <version>1.12</version>
+ <authors><author><name>Test Creator 12</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with a guid that matches an installed Addon should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test12.xpi</install>
+ </addon>
+
+ <!-- Fails because version is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test13@tests.mozilla.org</guid>
+ <authors><author><name>Test Creator 13</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test13.xpi</install>
+ </addon>
+
+ <!-- Fails because version is empty -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test14@tests.mozilla.org</guid>
+ <version> </version>
+ <authors><author><name>Test Creator 14</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with empty version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test14.xpi</install>
+ </addon>
+
+ <!-- Fails because authors undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test15@tests.mozilla.org</guid>
+ <version>1.15</version>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined authors should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test15.xpi</install>
+ </addon>
+
+ <!-- Fails because it has no defined author elements -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test16@tests.mozilla.org</guid>
+ <version>1.16</version>
+ <authors></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with no defined author elements should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test16.xpi</install>
+ </addon>
+
+ <!-- Fails because no non-empty author elements -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test17@tests.mozilla.org</guid>
+ <version>1.17</version>
+ <authors>
+ <author><name></name></author>
+ <author><name></name> </author>
+ </authors>
+ <status id="4">Public</status>
+ <summary>Add-on with no non-empty author elements should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test17.xpi</install>
+ </addon>
+
+ <!-- Fails because status is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test18@tests.mozilla.org</guid>
+ <version>1.18</version>
+ <authors><author><name>Test Creator 18</name></author></authors>
+ <summary>Add-on with undefined status should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test18.xpi</install>
+ </addon>
+
+ <!-- Fails because status is not Public -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test19@tests.mozilla.org</guid>
+ <version>1.19</version>
+ <authors><author><name>Test Creator 19</name></author></authors>
+ <status id="9999">Unknown</status>
+ <summary>Add-on with non-Public status should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test19.xpi</install>
+ </addon>
+
+ <!-- Fails because compatible_applications is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test20@tests.mozilla.org</guid>
+ <version>1.20</version>
+ <authors><author><name>Test Creator 20</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined compatible_applications should be ignored.</summary>
+ <install>http://localhost:%PORT%/test20.xpi</install>
+ </addon>
+
+ <!-- Fails because no compatible applications matched -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test21@tests.mozilla.org</guid>
+ <version>1.21</version>
+ <authors><author><name>Test Creator 21</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with no compatible applications should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>unknown@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test21.xpi</install>
+ </addon>
+
+ <!-- Fails because compatible application's min version is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test22@tests.mozilla.org</guid>
+ <version>1.22</version>
+ <authors><author><name>Test Creator 22</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with too high of a compatible application min version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <max_version>2.0</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test22.xpi</install>
+ </addon>
+
+ <!-- Fails because compatible application's min version too high -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test23@tests.mozilla.org</guid>
+ <version>1.23</version>
+ <authors><author><name>Test Creator 23</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with too high of a compatible application min version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1.1</min_version>
+ <max_version>2.0</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test23.xpi</install>
+ </addon>
+
+ <!-- Fails because compatible application's max version is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test24@tests.mozilla.org</guid>
+ <version>1.24</version>
+ <authors><author><name>Test Creator 24</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with too low of a compatible application max version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.9</min_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test24.xpi</install>
+ </addon>
+
+ <!-- Fails because compatible application's max version is too low -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test25@tests.mozilla.org</guid>
+ <version>1.25</version>
+ <authors><author><name>Test Creator 25</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with too low of a compatible application max version should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.9</min_version>
+ <max_version>0.9.9</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test25.xpi</install>
+ </addon>
+
+ <!-- Fails because XPI URL is undefined -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test26@tests.mozilla.org</guid>
+ <version>1.26</version>
+ <authors><author><name>Test Creator 26</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with undefined XPI URL should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ </addon>
+
+ <!-- Fails because XPI URL is empty -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test27@tests.mozilla.org</guid>
+ <version>1.27</version>
+ <authors><author><name>Test Creator 27</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with an empty XPI URL should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install> </install>
+ </addon>
+
+ <!-- Fails because install not compatible with OS -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test28@tests.mozilla.org</guid>
+ <version>1.28</version>
+ <authors><author><name>Test Creator 28</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with no installs with compatible OS should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install os="UNKNOWN1">http://localhost:%PORT%/test28.xpi</install>
+ <install os="UNKNOWN2">http://localhost:%PORT%/test28.xpi</install>
+ </addon>
+
+ <!-- Fails because XPI URL matches an installing AddonInstall -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test29@tests.mozilla.org</guid>
+ <version>1.29</version>
+ <authors><author><name>Test Creator 29</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with an XPI URL that matches an installing AddonInstall should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/addons/test_AddonRepository_2.xpi</install>
+ </addon>
+
+ <!-- Passes because the add-on has the right payment info -->
+ <addon>
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>purchase1@tests.mozilla.org</guid>
+ <version>2.0</version>
+ <authors>
+ <author>
+ <name>Test Creator - Last Passing</name>
+ <link>http://localhost:%PORT%/creatorLastPassing.html</link>
+ </author>
+ </authors>
+ <status id="4">Public</status>
+ <all_compatible_os>
+ <os>ALL</os>
+ </all_compatible_os>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <rating>5</rating>
+ <payment_data>
+ <link>http://localhost:%PORT%/purchaseURL1</link>
+ <amount amount="5">$5</amount>
+ </payment_data>
+ </addon>
+
+ <!-- Passes because the add-on has the right payment info -->
+ <addon>
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>purchase2@tests.mozilla.org</guid>
+ <version>2.0</version>
+ <authors>
+ <author>
+ <name>Test Creator - Last Passing</name>
+ <link>http://localhost:%PORT%/creatorLastPassing.html</link>
+ </author>
+ </authors>
+ <status id="4">Public</status>
+ <all_compatible_os>
+ <os>XPCShell</os>
+ </all_compatible_os>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <rating>5</rating>
+ <payment_data>
+ <link>http://localhost:%PORT%/purchaseURL2</link>
+ <amount amount="10.0">$10</amount>
+ </payment_data>
+ </addon>
+
+ <!-- Fails because the add-on doesn't match the platform -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>purchase3@tests.mozilla.org</guid>
+ <version>2.0</version>
+ <authors>
+ <author>
+ <name>Test Creator - Last Passing</name>
+ <link>http://localhost:%PORT%/creatorLastPassing.html</link>
+ </author>
+ </authors>
+ <status id="4">Public</status>
+ <all_compatible_os>
+ <os>FOO</os>
+ </all_compatible_os>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <rating>5</rating>
+ <payment_data>
+ <link>http://localhost:%PORT%/purchaseURL3</link>
+ <amount amount="10">$10</amount>
+ </payment_data>
+ </addon>
+
+ <!-- Passes because the Addon that has a matching XPI URL
+ has a state = STATE_AVAILABLE (non-active install). This is the
+ last passing add-on. -->
+ <addon>
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>test-lastPassing@tests.mozilla.org</guid>
+ <version>2.0</version>
+ <authors>
+ <author>
+ <name>Test Creator - Last Passing</name>
+ <link>http://localhost:%PORT%/creatorLastPassing.html</link>
+ </author>
+ </authors>
+ <status id="4">Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <!-- Test that a rating > 5 becomes a rating = 5 -->
+ <rating>10</rating>
+ <install>http://localhost:%PORT%/addons/test_AddonRepository_3.xpi</install>
+ </addon>
+
+ <!-- Fails because of MAX_RESULTS limit. The previous <addon> should
+ be the last passing add-on in order to correctly test the limit. -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test-surpassesLimit@tests.mozilla.org</guid>
+ <version>9.9</version>
+ <authors><author><name>Test Creator - Surpasses Limit</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on should not be added because doing so would surpass MAX_RESULTS limit</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test-surpassesLimit.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.xml
new file mode 100644
index 000000000..f707f1217
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_cache.xml
@@ -0,0 +1,182 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1111">
+ <addon>
+ <name>Repo Add-on 1</name>
+ <type id="1">Extension</type>
+ <guid>test_AddonRepository_1@tests.mozilla.org</guid>
+ <version>2.1</version>
+ <authors>
+ <author>
+ <name>Repo Add-on 1 - Creator</name>
+ <link>http://localhost:4444/repo/1/creator.html</link>
+ </author>
+ <author>
+ <name>Repo Add-on 1 - First Developer</name>
+ <link>http://localhost:4444/repo/1/firstDeveloper.html</link>
+ </author>
+ <author>
+ <name>Repo Add-on 1 - Second Developer</name>
+ <link>http://localhost:4444/repo/1/secondDeveloper.html</link>
+ </author>
+ </authors>
+ <summary>Repo Add-on 1 - Description&lt;br&gt;Second line</summary>
+ <description>&lt;p&gt;Repo Add-on 1 - Full Description &amp;amp; some extra&lt;/p&gt;</description>
+ <eula>Repo Add-on 1 - EULA</eula>
+ <developer_comments>Repo Add-on 1
+ Developer Comments</developer_comments>
+ <icon size="32">http://localhost/repo/1/icon.png</icon>
+ <status id="4">Public</status>
+ <rating>1</rating>
+ <learnmore>http://localhost/repo/1/learnmore.html</learnmore>
+ <homepage>http://localhost/repo/1/homepage.html</homepage>
+ <support>http://localhost/repo/1/support.html</support>
+ <contribution_data>
+ <link>http://localhost/repo/1/contribution.html</link>
+ <suggested_amount currency="USD">$11.11</suggested_amount>
+ <meet_developers>http://localhost/repo/1/meetDevelopers.html</meet_developers>
+ </contribution_data>
+ <reviews num="1111">http://localhost/repo/1/review.html</reviews>
+ <total_downloads>2221</total_downloads>
+ <weekly_downloads>3331</weekly_downloads>
+ <daily_users>4441</daily_users>
+ <last_updated epoch="9">1970-01-01T00:00:09Z</last_updated>
+ <install size="9">http://localhost:4444/repo/1/install.xpi</install>
+ </addon>
+
+ <addon>
+ <name>Repo Add-on 2</name>
+ <type id="2">Theme</type>
+ <guid>test_AddonRepository_2@tests.mozilla.org</guid>
+ <version>2.2</version>
+ <authors>
+ <author>
+ <name>Repo Add-on 2 - Creator</name>
+ <link>http://localhost:4444/repo/2/creator.html</link>
+ </author>
+ <author>
+ <name>Repo Add-on 2 - First Developer</name>
+ <link>http://localhost:4444/repo/2/firstDeveloper.html</link>
+ </author>
+ <author>
+ <name>Repo Add-on 2 - Second Developer</name>
+ <link>http://localhost:4444/repo/2/secondDeveloper.html</link>
+ </author>
+ </authors>
+ <summary>Repo Add-on 2 - Description</summary>
+ <description>Repo Add-on 2 - Full Description</description>
+ <eula>Repo Add-on 2 - EULA</eula>
+ <developer_comments>Repo Add-on 2 - Developer Comments</developer_comments>
+ <icon size="32">http://localhost/repo/2/icon.png</icon>
+ <status id="9">Unknown</status>
+ <previews>
+ <preview primary="1">
+ <full type="image/png">http://localhost:4444/repo/2/firstFull.png</full>
+ <thumbnail type="image/png">http://localhost:4444/repo/2/firstThumbnail.png</thumbnail>
+ <caption>Repo Add-on 2 - First Caption</caption>
+ </preview>
+ <preview primary="0">
+ <full type="image/png">http://localhost:4444/repo/2/secondFull.png</full>
+ <thumbnail type="image/png">http://localhost:4444/repo/2/secondThumbnail.png</thumbnail>
+ <caption>Repo Add-on 2 - Second Caption</caption>
+ </preview>
+ </previews>
+ <rating>2</rating>
+ <learnmore>http://localhost/repo/2/learnmore.html</learnmore>
+ <homepage>http://localhost/repo/2/homepage.html</homepage>
+ <support>http://localhost/repo/2/support.html</support>
+ <contribution_data>
+ <link>http://localhost/repo/2/contribution.html</link>
+ <meet_developers>http://localhost/repo/2/meetDevelopers.html</meet_developers>
+ </contribution_data>
+ <reviews num="1112">http://localhost/repo/2/review.html</reviews>
+ <total_downloads>2222</total_downloads>
+ <weekly_downloads>3332</weekly_downloads>
+ <daily_users>4442</daily_users>
+ <last_updated epoch="9">1970-01-01T00:00:09Z</last_updated>
+ <install size="9">http://localhost:4444/repo/2/install.xpi</install>
+ </addon>
+
+ <addon>
+ <name>Repo Add-on 3</name>
+ <type id="2">Theme</type>
+ <guid>test_AddonRepository_3@tests.mozilla.org</guid>
+ <version>2.3</version>
+ <icon size="32">http://localhost/repo/3/icon.png</icon>
+ <previews>
+ <preview primary="1">
+ <full type="image/png">http://localhost:4444/repo/3/firstFull.png</full>
+ <thumbnail type="image/png">http://localhost:4444/repo/3/firstThumbnail.png</thumbnail>
+ <caption>Repo Add-on 3 - First Caption</caption>
+ </preview>
+ <preview primary="0">
+ <full type="image/png">http://localhost:4444/repo/3/secondFull.png</full>
+ <thumbnail type="image/png">http://localhost:4444/repo/3/secondThumbnail.png</thumbnail>
+ <caption>Repo Add-on 3 - Second Caption</caption>
+ </preview>
+ </previews>
+ </addon>
+
+ <addon_compatibility hosted="true" id="123">
+ <guid>test_AddonRepository_1@tests.mozilla.org</guid>
+ <name>PASS</name>
+ <version_ranges>
+ <!-- Will be included -->
+ <version_range type="incompatible">
+ <min_version>0.1</min_version>
+ <max_version>0.2</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>3.0</min_version>
+ <max_version>4.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Will be included -->
+ <version_range type="incompatible">
+ <min_version>0.2</min_version>
+ <max_version>0.3</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>5.0</min_version>
+ <max_version>6.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Won't be included - invalid type attribute -->
+ <version_range type="unknown">
+ <min_version>9</min_version>
+ <max_version>10</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>10.0</min_version>
+ <max_version>11.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Won't be included - no matching appID -->
+ <version_range type="incompatible">
+ <min_version>0.2</min_version>
+ <max_version>0.3</max_version>
+ <compatible_applications>
+ <application>
+ <name>Unknown App</name>
+ <application_id>123</application_id>
+ <min_version>1.0</min_version>
+ <max_version>999.0</max_version>
+ <appID>unknown-app@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_ignore.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_ignore.xml
new file mode 100644
index 000000000..003095727
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_ignore.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="123">
+ <addon>
+ <name>Test Repo Add-on - ignore</name>
+ <type id="1">Extension</type>
+ <guid>compatmode-ignore@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors>
+ <author>
+ <name>Test Creator 1</name>
+ <link>http://localhost:%PORT%/creator1.html</link>
+ </author>
+ </authors>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test1.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_normal.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_normal.xml
new file mode 100644
index 000000000..fec8b09ca
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_normal.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="123">
+ <addon>
+ <name>Test Repo Add-on - normal</name>
+ <type id="1">Extension</type>
+ <guid>compatmode-normal@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors>
+ <author>
+ <name>Test Creator 1</name>
+ <link>http://localhost:%PORT%/creator1.html</link>
+ </author>
+ </authors>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test1.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_strict.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_strict.xml
new file mode 100644
index 000000000..f99256b87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_compatmode_strict.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="123">
+ <addon>
+ <name>Test Repo Add-on - strict</name>
+ <type id="1">Extension</type>
+ <guid>compatmode-strict@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors>
+ <author>
+ <name>Test Creator 1</name>
+ <link>http://localhost:%PORT%/creator1.html</link>
+ </author>
+ </authors>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test1.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.xml
new file mode 100644
index 000000000..4cd5c1443
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_empty.xml
@@ -0,0 +1,3 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="9999" />
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_failed.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_failed.xml
new file mode 100644
index 000000000..d02fa0249
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_failed.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="9999">
+<!-- Cause XML parse error so that the search fails -->
+<!-- <addon> -->
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors><author><name>Test Creator 1</name></author></authors>
+ <status id="4">Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test1.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml
new file mode 100644
index 000000000..8a6167969
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_AddonRepository_getAddonsByIDs.xml
@@ -0,0 +1,187 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1111">
+ <!-- Passes even though XPI URL matches an installing AddonInstall.
+ Tests optional attributes. -->
+ <addon>
+ <name>PASS</name>
+ <type id="1">Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.1</version>
+ <authors>
+ <author>
+ <name>Test Creator 1</name>
+ <link>http://localhost:%PORT%/creator1.html</link>
+ </author>
+ <author>
+ <name>Test Developer 1</name>
+ <link>http://localhost:%PORT%/developer1.html</link>
+ </author>
+ </authors>
+ <summary>Test Summary 1</summary>
+ <description>Test Description 1</description>
+ <eula>Test EULA 1</eula>
+ <developer_comments>Test Developer Comments 1</developer_comments>
+ <icon size="32">http://localhost:%PORT%/icon1.png</icon>
+ <status id="8">Preliminarily Reviewed</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <previews>
+ <preview primary="1">
+ <full type="image/png" width="400" height="300">
+ http://localhost:%PORT%/full1-1.png
+ </full>
+ <thumbnail type="image/png" width="200" height="150">
+ http://localhost:%PORT%/thumbnail1-1.png
+ </thumbnail>
+ <caption>Caption 1 - 1</caption>
+ </preview>
+ <preview primary="0">
+ <full type="image/png">http://localhost:%PORT%/full2-1.png</full>
+ <thumbnail type="image/png">http://localhost:%PORT%/thumbnail2-1.png</thumbnail>
+ <caption>Caption 2 - 1</caption>
+ </preview>
+ </previews>
+ <rating>4</rating>
+ <learnmore>http://localhost:%PORT%/learnmore1.html</learnmore>
+ <support>http://localhost:%PORT%/support1.html</support>
+ <contribution_data>
+ <link>http://localhost:%PORT%/contribution1.html</link>
+ <suggested_amount currency="USD">$11.11</suggested_amount>
+ <meet_developers>http://localhost:%PORT%/meetDevelopers1.html</meet_developers>
+ </contribution_data>
+ <reviews num="1111">http://localhost:%PORT%/review1.html</reviews>
+ <total_downloads>2222</total_downloads>
+ <weekly_downloads>3333</weekly_downloads>
+ <daily_users>4444</daily_users>
+ <last_updated epoch="1265033045">2010-02-01T14:04:05Z</last_updated>
+ <install size="5555">http://localhost:%PORT%/addons/test_AddonRepository_2.xpi</install>
+ </addon>
+
+ <addon_compatibility hosted="true" id="123">
+ <guid>test1@tests.mozilla.org</guid>
+ <name>PASS</name>
+ <version_ranges>
+ <!-- Will be included -->
+ <version_range type="incompatible">
+ <min_version>0.1</min_version>
+ <max_version>0.2</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>3.0</min_version>
+ <max_version>4.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Will be included -->
+ <version_range type="incompatible">
+ <min_version>0.2</min_version>
+ <max_version>0.3</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>5.0</min_version>
+ <max_version>6.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Won't be included - invalid type attribute -->
+ <version_range type="unknown">
+ <min_version>9</min_version>
+ <max_version>10</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <application_id>666</application_id>
+ <min_version>10.0</min_version>
+ <max_version>11.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <!-- Won't be included - no matching appID -->
+ <version_range type="incompatible">
+ <min_version>0.2</min_version>
+ <max_version>0.3</max_version>
+ <compatible_applications>
+ <application>
+ <name>Unknown App</name>
+ <application_id>123</application_id>
+ <min_version>1.0</min_version>
+ <max_version>999.0</max_version>
+ <appID>unknown-app@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <!-- Fails because guid matches previously successful result -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.2</version>
+ <authors><author><name>Test Creator 2</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with a guid that matches a previously successful result should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test2.xpi</install>
+ </addon>
+
+ <!-- Fails because guid was not requested -->
+ <addon>
+ <name>FAIL</name>
+ <type id="1">Extension</type>
+ <guid>notRequested@tests.mozilla.org</guid>
+ <version>1.3</version>
+ <authors><author><name>Test Creator 3</name></author></authors>
+ <status id="4">Public</status>
+ <summary>Add-on with a guid that wasn't requested should be ignored.</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test3.xpi</install>
+ </addon>
+
+ <!-- Passes even though guid matches already installed add-on,
+ type is unknown, no defined author elements, status is not Public,
+ no compatible applications matched, no installs compatible with OS
+ -->
+ <addon>
+ <name>PASS</name>
+ <type id="2">Theme</type>
+ <guid>test_AddonRepository_1@tests.mozilla.org</guid>
+ <version>1.4</version>
+ <status id="9999">Unknown</status>
+ <compatible_applications>
+ <application>
+ <appID>unknown@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install os="UNKNOWN1">http://localhost:%PORT%/test4.xpi</install>
+ <install os="UNKNOWN2">http://localhost:%PORT%/test4.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.rdf
new file mode 100644
index 000000000..ab7cdef34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_backgroundupdate.rdf
@@ -0,0 +1,70 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:addon1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- app id compatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:addon2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- app id compatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:addon3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- app id compatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml
new file mode 100644
index 000000000..368a6ed53
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_metadata_filters_1.xml
@@ -0,0 +1,21 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem name="/^Mozilla Corp\.$/">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="/block2/" name="/^Moz/" creator="Dangerous"
+ homepageURL="/\.dangerous\.com/" updateURL="/\.dangerous\.com/">
+ <versionRange severity="3">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml
new file mode 100644
index 000000000..41df457b0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_prefs_1.xml
@@ -0,0 +1,28 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="block1@tests.mozilla.org">
+ <prefs>
+ <pref>test.blocklist.pref1</pref>
+ <pref>test.blocklist.pref2</pref>
+ </prefs>
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="block2@tests.mozilla.org">
+ <prefs>
+ <pref>test.blocklist.pref3</pref>
+ <pref>test.blocklist.pref4</pref>
+ </prefs>
+ <versionRange severity="3">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_regexp_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_regexp_1.xml
new file mode 100644
index 000000000..20035c6a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_blocklist_regexp_1.xml
@@ -0,0 +1,20 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="/block1/">
+ <versionRange severity="1">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="/block1/">
+ <versionRange severity="2">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="2.*"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716.rdf
new file mode 100644
index 000000000..d60d8ca3f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716.rdf
@@ -0,0 +1,181 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE RDF:RDF [
+ <!ENTITY bug299716 "urn:mozilla:extension:bug299716">
+ <!ENTITY addons_prefix "http://localhost:4444/addons/test_bug299716">
+ <!ENTITY v0.2 "<em:version>0.2</em:version>">
+
+ <!ENTITY xpcshell.app "
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ ">
+
+ <!ENTITY toolkit.app "
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ ">
+
+ <!ENTITY invalidRange "
+ <em:minVersion>30</em:minVersion>
+ <em:maxVersion>30</em:maxVersion>
+ ">
+
+ <!ENTITY xpcshell.invalid "
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ &invalidRange;
+ ">
+
+ <!ENTITY toolkit.invalid "
+ <em:id>toolkit@mozilla.org</em:id>
+ &invalidRange;
+ ">
+]>
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <!-- XPCShell -->
+ <RDF:Description about="&bug299716;-a@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-a@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-a@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_a_2.xpi">
+ &xpcshell.app;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- Toolkit -->
+ <RDF:Description about="&bug299716;-b@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-b@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-b@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_b_2.xpi">
+ &toolkit.app;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- XPCShell + Toolkit -->
+ <RDF:Description about="&bug299716;-c@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-c@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-c@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_c_2.xpi">
+ &xpcshell.app;
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_c_2.xpi">
+ &toolkit.app;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- XPCShell (Toolkit invalid) -->
+ <RDF:Description about="&bug299716;-d@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-d@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-d@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_d_2.xpi">
+ &xpcshell.app;
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_d_2.xpi">
+ &toolkit.invalid;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- Toolkit (XPCShell invalid), should not install -->
+ <RDF:Description about="&bug299716;-e@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-e@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-e@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_e_2.xpi">
+ &xpcshell.invalid;
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_e_2.xpi">
+ &toolkit.app;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- None (XPCShell, Toolkit invalid), should not install -->
+ <RDF:Description about="&bug299716;-f@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-f@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-f@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_f_2.xpi">
+ &xpcshell.invalid;
+ </RDF:Description>
+ </em:targetApplication>
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_f_2.xpi">
+ &toolkit.invalid;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+
+ <!-- Toolkit (invalid), should not install -->
+ <RDF:Description about="&bug299716;-g@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li RDF:resource="&bug299716;-g@tests.mozilla.org:0.2"/>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="&bug299716;-g@tests.mozilla.org:0.2">
+ &v0.2;
+ <em:targetApplication>
+ <RDF:Description em:updateLink="&addons_prefix;_g_2.xpi">
+ &toolkit.invalid;
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716_2.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716_2.rdf
new file mode 100644
index 000000000..94a4ea450
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug299716_2.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+ <RDF:Description about="urn:mozilla:extension:bug299716-2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>0.1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>2.0.*</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug324121.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug324121.rdf
new file mode 100644
index 000000000..2c453f756
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug324121.rdf
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:bug324121_2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- app id compatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>3</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug324121_3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- app id incompatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug324121_6@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- toolkit id compatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>3</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug324121_7@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- toolkit id incompatible update available -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml
new file mode 100644
index 000000000..1767b4332
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug393285.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="test_bug393285_2@tests.mozilla.org"/>
+ <emItem id="test_bug393285_3a@tests.mozilla.org">
+ <versionRange minVersion="1.0" maxVersion="1.0"/>
+ </emItem>
+ <emItem id="test_bug393285_3b@tests.mozilla.org">
+ <versionRange minVersion="1.0" maxVersion="1.0"/>
+ </emItem>
+ <emItem id="test_bug393285_4@tests.mozilla.org">
+ <versionRange minVersion="1.0" maxVersion="1.0">
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1.0" maxVersion="1.0"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug393285_5@tests.mozilla.org" os="Darwin"/>
+ <emItem id="test_bug393285_6@tests.mozilla.org" os="XPCShell"/>
+ <emItem id="test_bug393285_7@tests.mozilla.org" os="Darwin,XPCShell,WINNT"/>
+ <emItem id="test_bug393285_8@tests.mozilla.org" xpcomabi="x86-msvc"/>
+ <emItem id="test_bug393285_9@tests.mozilla.org" xpcomabi="noarch-spidermonkey"/>
+ <emItem id="test_bug393285_10@tests.mozilla.org" xpcomabi="ppc-gcc3,noarch-spidermonkey,x86-msvc"/>
+ <emItem id="test_bug393285_11@tests.mozilla.org" os="Darwin" xpcomabi="ppc-gcc3,x86-msvc"/>
+ <emItem id="test_bug393285_12@tests.mozilla.org" os="Darwin" xpcomabi="ppc-gcc3,noarch-spidermonkey,x86-msvc"/>
+ <emItem id="test_bug393285_13@tests.mozilla.org" os="XPCShell" xpcomabi="ppc-gcc3,x86-msvc"/>
+ <emItem id="test_bug393285_14@tests.mozilla.org" os="XPCShell,WINNT" xpcomabi="ppc-gcc3,x86-msvc,noarch-spidermonkey"/>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug394300.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug394300.rdf
new file mode 100644
index 000000000..94e12527f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug394300.rdf
@@ -0,0 +1,159 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:bug394300_1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- Not a valid install - incompatible app versions -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>20</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Valid install should be the version detected -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>10</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Valid install. Detecting this would indicate that the order
+ of entries is playing a part in the update detection. -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>6</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Not a valid install - no minVersion or maxVersion specified -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>40</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Not a valid install - incompatible app versions -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>30</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <RDF:Description about="urn:mozilla:extension:bug394300_2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <!-- Not a valid install - incompatible app versions -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>20</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Valid install should be the version detected -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>10</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Valid install. Detecting this would indicate that the order
+ of entries is playing a part in the update detection. -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>6</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1.9</em:minVersion>
+ <em:maxVersion>1.9</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Not a valid install - no minVersion or maxVersion specified -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>40</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ <!-- Not a valid install - incompatible app versions -->
+ <RDF:li>
+ <RDF:Description>
+ <em:version>30</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug424262.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug424262.xml
new file mode 100644
index 000000000..d797debbb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug424262.xml
@@ -0,0 +1,185 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>TEST</name>
+ <type id='1'>Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>-5</rating>
+ <type id='1'>Extension</type>
+ <guid>test2@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>0</rating>
+ <type id='1'>Extension</type>
+ <guid>test3@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>2</rating>
+ <type id='1'>Extension</type>
+ <guid>test4@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>4</rating>
+ <type id='1'>Extension</type>
+ <guid>test5@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>5</rating>
+ <type id='1'>Extension</type>
+ <guid>test6@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>10</rating>
+ <type id='1'>Extension</type>
+ <guid>test7@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>TEST</name>
+ <rating>100</rating>
+ <type id='1'>Extension</type>
+ <guid>test8@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml
new file mode 100644
index 000000000..f12ca1fa6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_app.xml
@@ -0,0 +1,333 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <!-- All extensions are version 5 and tests run against appVersion 3 -->
+
+ <!-- Test 1 not listed, should never get blocked -->
+ <!-- Always blocked -->
+ <emItem id="test_bug449027_2@tests.mozilla.org"/>
+ <!-- Always blocked -->
+ <emItem id="test_bug449027_3@tests.mozilla.org">
+ <versionRange/>
+ </emItem>
+ <!-- Not blocked since neither version range matches -->
+ <emItem id="test_bug449027_4@tests.mozilla.org">
+ <versionRange minVersion="6"/>
+ <versionRange maxVersion="4"/>
+ </emItem>
+ <!-- Invalid version range, should not block -->
+ <emItem id="test_bug449027_5@tests.mozilla.org">
+ <versionRange minVersion="6" maxVersion="4"/>
+ </emItem>
+ <!-- Should block all of these -->
+ <emItem id="test_bug449027_6@tests.mozilla.org">
+ <versionRange minVersion="7" maxVersion="8"/>
+ <versionRange minVersion="5" maxVersion="6"/>
+ <versionRange maxVersion="4"/>
+ </emItem>
+ <emItem id="test_bug449027_7@tests.mozilla.org">
+ <versionRange maxVersion="4"/>
+ <versionRange minVersion="4" maxVersion="5"/>
+ <versionRange minVersion="6" maxVersion="7"/>
+ </emItem>
+ <emItem id="test_bug449027_8@tests.mozilla.org">
+ <versionRange minVersion="2" maxVersion="2"/>
+ <versionRange minVersion="4" maxVersion="6"/>
+ <versionRange minVersion="7" maxVersion="8"/>
+ </emItem>
+ <emItem id="test_bug449027_9@tests.mozilla.org">
+ <versionRange minVersion="4"/>
+ </emItem>
+ <emItem id="test_bug449027_10@tests.mozilla.org">
+ <versionRange minVersion="5"/>
+ </emItem>
+ <emItem id="test_bug449027_11@tests.mozilla.org">
+ <versionRange maxVersion="6"/>
+ </emItem>
+ <emItem id="test_bug449027_12@tests.mozilla.org">
+ <versionRange maxVersion="5"/>
+ </emItem>
+
+ <!-- This should block all versions for any application -->
+ <emItem id="test_bug449027_13@tests.mozilla.org">
+ <versionRange>
+ <targetApplication/>
+ </versionRange>
+ </emItem>
+ <!-- Shouldn't block -->
+ <emItem id="test_bug449027_14@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </emItem>
+ <!-- Should block for any version of the app -->
+ <emItem id="test_bug449027_15@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org"/>
+ </versionRange>
+ </emItem>
+ <!-- Should still block -->
+ <emItem id="test_bug449027_16@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Not blocked since neither version range matches -->
+ <emItem id="test_bug449027_17@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="4"/>
+ <versionRange maxVersion="2"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Invalid version range, should not block -->
+ <emItem id="test_bug449027_18@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="6" maxVersion="4"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Should block all of these -->
+ <emItem id="test_bug449027_19@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="5" maxVersion="6"/>
+ <versionRange minVersion="3" maxVersion="4"/>
+ <versionRange maxVersion="2"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_20@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="2"/>
+ <versionRange minVersion="2" maxVersion="3"/>
+ <versionRange minVersion="4" maxVersion="5"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_21@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="1"/>
+ <versionRange minVersion="2" maxVersion="4"/>
+ <versionRange minVersion="5" maxVersion="6"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_22@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="3"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_23@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_24@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="3"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_25@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="4"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+ <pluginItems>
+ <!-- All plugins are version 5 and tests run against appVersion 3 -->
+
+ <!-- Test 1 not listed, should never get blocked -->
+ <!-- Always blocked -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_2$"/>
+ </pluginItem>
+ <!-- Always blocked -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_3$"/>
+ <versionRange/>
+ </pluginItem>
+ <!-- Not blocked since neither version range matches -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_4$"/>
+ <versionRange minVersion="6"/>
+ <versionRange maxVersion="4"/>
+ </pluginItem>
+ <!-- Invalid version range, should not block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_5$"/>
+ <versionRange minVersion="6" maxVersion="4"/>
+ </pluginItem>
+ <!-- Should block all of these -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_6$"/>
+ <versionRange minVersion="7" maxVersion="8"/>
+ <versionRange minVersion="5" maxVersion="6"/>
+ <versionRange maxVersion="4"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_7$"/>
+ <versionRange maxVersion="4"/>
+ <versionRange minVersion="4" maxVersion="5"/>
+ <versionRange minVersion="6" maxVersion="7"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_8$"/>
+ <versionRange minVersion="2" maxVersion="2"/>
+ <versionRange minVersion="4" maxVersion="6"/>
+ <versionRange minVersion="7" maxVersion="8"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_9$"/>
+ <versionRange minVersion="4"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_10$"/>
+ <versionRange minVersion="5"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_11$"/>
+ <versionRange maxVersion="6"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_12$"/>
+ <versionRange maxVersion="5"/>
+ </pluginItem>
+
+ <!-- This should block all versions for any application -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_13$"/>
+ <versionRange>
+ <targetApplication/>
+ </versionRange>
+ </pluginItem>
+ <!-- Shouldn't block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_14$"/>
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </pluginItem>
+ <!-- Should block for any version of the app -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_15$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org"/>
+ </versionRange>
+ </pluginItem>
+ <!-- Should still block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_16$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Not blocked since neither version range matches -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_17$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="4"/>
+ <versionRange maxVersion="2"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Invalid version range, should not block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_18$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="6" maxVersion="4"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Should block all of these -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_19$"/>
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="5" maxVersion="6"/>
+ <versionRange minVersion="3" maxVersion="4"/>
+ <versionRange maxVersion="2"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_20$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="2"/>
+ <versionRange minVersion="2" maxVersion="3"/>
+ <versionRange minVersion="4" maxVersion="5"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_21$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="1" maxVersion="1"/>
+ <versionRange minVersion="2" maxVersion="4"/>
+ <versionRange minVersion="5" maxVersion="6"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_22$"/>
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="3"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_23$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange minVersion="2"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_24$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="3"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_25$"/>
+ <versionRange>
+ <targetApplication id="xpcshell@tests.mozilla.org">
+ <versionRange maxVersion="4"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml
new file mode 100644
index 000000000..ad8ec5ed9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug449027_toolkit.xml
@@ -0,0 +1,208 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <!-- All extensions are version 5 and tests run against toolkitVersion 8 -->
+
+ <!-- Test 1-14 not listed, should never get blocked -->
+
+ <!-- Should block for any version of the app -->
+ <emItem id="test_bug449027_15@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org"/>
+ </versionRange>
+ </emItem>
+ <!-- Should still block -->
+ <emItem id="test_bug449027_16@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Not blocked since neither version range matches -->
+ <emItem id="test_bug449027_17@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="9"/>
+ <versionRange maxVersion="7"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Invalid version range, should not block -->
+ <emItem id="test_bug449027_18@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="11" maxVersion="9"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <!-- Should block all of these -->
+ <emItem id="test_bug449027_19@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="10" maxVersion="11"/>
+ <versionRange minVersion="8" maxVersion="9"/>
+ <versionRange maxVersion="7"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_20@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="7"/>
+ <versionRange minVersion="7" maxVersion="8"/>
+ <versionRange minVersion="9" maxVersion="10"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_21@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="6" maxVersion="6"/>
+ <versionRange minVersion="7" maxVersion="9"/>
+ <versionRange minVersion="10" maxVersion="11"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_22@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="8"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_23@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="7"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_24@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="8"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ <emItem id="test_bug449027_25@tests.mozilla.org">
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="9"/>
+ </targetApplication>
+ </versionRange>
+ </emItem>
+ </emItems>
+ <pluginItems>
+ <!-- All plugins are version 5 and tests run against appVersion 3 -->
+
+ <!-- Test 1-14 not listed, should never get blocked -->
+ <!-- Should block for any version of the app -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_15$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org"/>
+ </versionRange>
+ </pluginItem>
+ <!-- Should still block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_16$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Not blocked since neither version range matches -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_17$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="9"/>
+ <versionRange maxVersion="7"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Invalid version range, should not block -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_18$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="11" maxVersion="9"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <!-- Should block all of these -->
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_19$"/>
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="10" maxVersion="11"/>
+ <versionRange minVersion="8" maxVersion="9"/>
+ <versionRange maxVersion="7"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_20$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="7"/>
+ <versionRange minVersion="7" maxVersion="8"/>
+ <versionRange minVersion="9" maxVersion="10"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_21$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="6" maxVersion="6"/>
+ <versionRange minVersion="7" maxVersion="9"/>
+ <versionRange minVersion="10" maxVersion="11"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_22$"/>
+ <versionRange>
+ <targetApplication id="foo@bar.com"/>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="8"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_23$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange minVersion="7"/>
+ </targetApplication>
+ <targetApplication id="foo@bar.com"/>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_24$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="8"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug449027_25$"/>
+ <versionRange>
+ <targetApplication id="toolkit@mozilla.org">
+ <versionRange maxVersion="9"/>
+ </targetApplication>
+ </versionRange>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml
new file mode 100644
index 000000000..85f0da57c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug468528.xml
@@ -0,0 +1,15 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^test_bug468528_1"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug468528_2["/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug468528_3"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_1.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_1.rdf
new file mode 100644
index 000000000..5397e8a87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_1.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_1@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_2.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_2.rdf
new file mode 100644
index 000000000..b1dde7f7a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_2.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_2@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_3.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_3.rdf
new file mode 100644
index 000000000..ae483434a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_3.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_3@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_4.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_4.rdf
new file mode 100644
index 000000000..97abacc5e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_4.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_4@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_5.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_5.rdf
new file mode 100644
index 000000000..bff1104a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/install_5.rdf
@@ -0,0 +1,17 @@
+<?xml version="1.0"?>
+
+<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>bug470377_5@tests.mozilla.org</em:id>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ <em:name>Test for Bug 470377</em:name>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_1.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_1.rdf
new file mode 100644
index 000000000..e4ad91ae9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_1.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:test_bug470377_1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>unknown@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_2.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_2.rdf
new file mode 100644
index 000000000..10fcafd39
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_2.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:test_bug470377_2@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_3.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_3.rdf
new file mode 100644
index 000000000..684002462
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_3.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:test_bug470377_3@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_4.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_4.rdf
new file mode 100644
index 000000000..6e7116239
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_4.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:test_bug470377_4@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_5.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_5.rdf
new file mode 100644
index 000000000..c926af934
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug470377/update_5.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:test_bug470377_5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml
new file mode 100644
index 000000000..c4cc2fe37
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_1.xml
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^test_bug514327_1"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug514327_2"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_bug514327_3"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml
new file mode 100644
index 000000000..cc0a0c69d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_2.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="Test Plug-in"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml
new file mode 100644
index 000000000..0261794f8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_empty.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml
new file mode 100644
index 000000000..d651f8799
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_1.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="test_bug514327_1"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="test_bug514327_outdated"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml
new file mode 100644
index 000000000..208444681
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug514327_3_outdated_2.xml
@@ -0,0 +1,13 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="test_bug514327_2"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="test_bug514327_outdated"/>
+ <versionRange severity="0"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_1.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_1.xpi
new file mode 100644
index 000000000..2dbcc0b50
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_1.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_2.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_2.xpi
new file mode 100644
index 000000000..86fc6baaa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug526598_2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug541420.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug541420.xpi
new file mode 100644
index 000000000..adb7be9ad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug541420.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug542391.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug542391.rdf
new file mode 100644
index 000000000..db82cf675
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug542391.rdf
@@ -0,0 +1,25 @@
+<?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:extension:override1x2-1x3@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug554133.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug554133.xml
new file mode 100644
index 000000000..736c10514
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug554133.xml
@@ -0,0 +1,292 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="100">
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test1@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>FAIL</name>
+ <type id='1'>Extension</type>
+ <guid>test2@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Should not return an incompatible add-on</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>2</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test3@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>FAIL</name>
+ <type id='1'>Extension</type>
+ <guid>test4@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Should not return an add-on for a different OS</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install os="UNKNOWN">http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test5@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>FAIL</name>
+ <type id='1'>Extension</type>
+ <guid>test5@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Should not include the same result twice</summary>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test6@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test7@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test8@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test9@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test10@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test11@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+
+ <addon>
+ <name>PASS</name>
+ <type id='1'>Extension</type>
+ <guid>test12@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://localhost:%PORT%/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://localhost:%PORT%/test.xpi</install>
+ </addon>
+</searchresults>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug619730.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug619730.xml
new file mode 100644
index 000000000..f2511c0de
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug619730.xml
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <gfxItems testattr="GFX"><gfxItem/><gfxItem/></gfxItems>
+ <testItems testattr="FOO"><testItem/><testItem/><testItem/></testItems>
+ <fooItems/>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.rdf
new file mode 100644
index 000000000..9857dcb55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_bug655254.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF:RDF xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <RDF:Description about="urn:mozilla:extension:addon1@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>1</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+</RDF:RDF>
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_compatoverrides.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_compatoverrides.xml
new file mode 100644
index 000000000..c0d67d033
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_compatoverrides.xml
@@ -0,0 +1,228 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="9">
+ <addon>
+ <name>Test addon 2</name>
+ <type id="1">Extension</type>
+ <guid>addon2@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+
+ <addon>
+ <name>Test addon 3</name>
+ <type id="1">Extension</type>
+ <guid>addon3@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 3</name>
+ <guid>addon3@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.9</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon>
+ <name>Test addon 4</name>
+ <type id="1">Extension</type>
+ <guid>addon4@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 4</name>
+ <guid>addon4@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.9</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon>
+ <name>Test addon 5</name>
+ <type id="1">Extension</type>
+ <guid>addon5@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 5</name>
+ <guid>addon5@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.9</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>Unknown App</name>
+ <appID>unknown-app@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon>
+ <name>Test addon 6</name>
+ <type id="1">Extension</type>
+ <guid>addon6@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 6</name>
+ <guid>addon6@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.5</min_version>
+ <max_version>0.9</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon>
+ <name>Test addon 7</name>
+ <type id="1">Extension</type>
+ <guid>addon7@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 7</name>
+ <guid>addon7@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.5</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.1</min_version>
+ <max_version>0.9</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon>
+ <name>Test addon 8</name>
+ <type id="1">Extension</type>
+ <guid>addon8@tests.mozilla.org</guid>
+ <version>1.0</version>
+ </addon>
+ <addon_compatibility hosted="true">
+ <name>Test addon 8</name>
+ <guid>addon8@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>6</min_version>
+ <max_version>6.2</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.9</min_version>
+ <max_version>9</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <version_range type="incompatible">
+ <min_version>0.5</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.1</min_version>
+ <max_version>9</max_version>
+ </application>
+ <application>
+ <name>Unknown app</name>
+ <appID>unknown-app@tests.mozilla.org</appID>
+ <min_version>0.1</min_version>
+ <max_version>9</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ <version_range type="incompatible">
+ <min_version>0.1</min_version>
+ <max_version>0.2</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>0.1</min_version>
+ <max_version>0.9</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon_compatibility hosted="false">
+ <name>Test addon 9</name>
+ <guid>addon9@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>0.5</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+ <addon_compatibility hosted="false">
+ <name>Test addon 10</name>
+ <guid>addon10@tests.mozilla.org</guid>
+ <version_ranges>
+ <version_range type="compatible">
+ <min_version>0.5</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>2</max_version>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.rdf
new file mode 100644
index 000000000..f3341bdcf
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_corrupt.rdf
@@ -0,0 +1,44 @@
+<?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:extension:addon3@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon4@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_complete/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_complete/bootstrap.js
new file mode 100644
index 000000000..a81c90bf0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_complete/bootstrap.js
@@ -0,0 +1,24 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+const INSTALL_COMPLETE_PREF = "bootstraptest.install_complete_done";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ // apply update immediately
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ upgrade.install();
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_defer/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_defer/bootstrap.js
new file mode 100644
index 000000000..25ffd8565
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_defer/bootstrap.js
@@ -0,0 +1,34 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_complete@tests.mozilla.org";
+const INSTALL_COMPLETE_PREF = "bootstraptest.install_complete_done";
+
+// global reference to hold upgrade object
+let gUpgrade;
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ // do not apply update immediately, hold on to for later
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ gUpgrade = upgrade;
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+
+ // add a listener so the test can pass control back
+ AddonManager.addAddonListener({
+ onFakeEvent: () => {
+ gUpgrade.install();
+ }
+ })
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_ignore/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_ignore/bootstrap.js
new file mode 100644
index 000000000..7693c9c2d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_update_ignore/bootstrap.js
@@ -0,0 +1,26 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/AddonManager.jsm");
+
+const ADDON_ID = "test_delay_update_ignore@tests.mozilla.org";
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+function install(data, reason) {}
+
+// normally we would use BootstrapMonitor here, but we need a reference to
+// the symbol inside `XPIProvider.jsm`.
+function startup(data, reason) {
+ Services.prefs.setBoolPref(TEST_IGNORE_PREF, false);
+
+ // explicitly ignore update, will be queued for next restart
+ if (data.hasOwnProperty("instanceID") && data.instanceID) {
+ AddonManager.addUpgradeListener(data.instanceID, (upgrade) => {
+ Services.prefs.setBoolPref(TEST_IGNORE_PREF, true);
+ });
+ } else {
+ throw Error("no instanceID passed to bootstrap startup");
+ }
+}
+
+function shutdown(data, reason) {}
+
+function uninstall(data, reason) {}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json
new file mode 100644
index 000000000..cf3defdc7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.json
@@ -0,0 +1,11 @@
+{
+ "addons": {
+ "test_delay_update_complete_webext@tests.mozilla.org": {
+ "updates": [
+ { "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_delay_update_complete_webextension_v2.xpi"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.rdf
new file mode 100644
index 000000000..8af39cb0e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_complete.rdf
@@ -0,0 +1,26 @@
+<?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:extension:test_delay_update_complete@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_complete_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json
new file mode 100644
index 000000000..2fcab10b5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.json
@@ -0,0 +1,11 @@
+{
+ "addons": {
+ "test_delay_update_defer_webext@tests.mozilla.org": {
+ "updates": [
+ { "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_delay_update_defer_webextension_v2.xpi"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.rdf
new file mode 100644
index 000000000..d44d4880f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_defer.rdf
@@ -0,0 +1,26 @@
+<?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:extension:test_delay_update_defer@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_defer_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json
new file mode 100644
index 000000000..b7f48149d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.json
@@ -0,0 +1,11 @@
+{
+ "addons": {
+ "test_delay_update_ignore_webext@tests.mozilla.org": {
+ "updates": [
+ { "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_delay_update_ignore_webextension_v2.xpi"
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.rdf
new file mode 100644
index 000000000..866884f8d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_delay_updates_ignore.rdf
@@ -0,0 +1,26 @@
+<?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:extension:test_delay_update_ignore@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- app id compatible update available -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_delay_update_ignore_v2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_dictionary.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_dictionary.rdf
new file mode 100644
index 000000000..364b3bba1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_dictionary.rdf
@@ -0,0 +1,65 @@
+<?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:extension:ab-CD@dictionaries.addons.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_dictionary_3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:ef@dictionaries.addons.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_dictionary_4.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:gh@dictionaries.addons.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_dictionary_5.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/bootstrap.js
new file mode 100644
index 000000000..01682d3b7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/bootstrap.js
@@ -0,0 +1,21 @@
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function install(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", 2);
+ Services.prefs.setIntPref("bootstraptest.install_reason", reason);
+}
+
+function startup(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 2);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", reason);
+}
+
+function shutdown(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.active_version", 0);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", reason);
+}
+
+function uninstall(data, reason) {
+ Services.prefs.setIntPref("bootstraptest.installed_version", 0);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", reason);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/install.rdf
new file mode 100644
index 000000000..ebe547ccc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<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>addon2@tests.mozilla.org</em:id>
+ <em:version>2.0</em:version>
+
+ <!-- Front End MetaData -->
+ <em:name>Distributed add-ons test</em:name>
+ <em:bootstrap>true</em:bootstrap>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>5</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/dummy.txt b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/dummy.txt
new file mode 100644
index 000000000..051aef85a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/dummy.txt
@@ -0,0 +1 @@
+Test of a file in a sub directory \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/subdir2/dummy2.txt b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/subdir2/dummy2.txt
new file mode 100644
index 000000000..9eddc4493
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_distribution2_2/subdir/subdir2/dummy2.txt
@@ -0,0 +1 @@
+Nested dummy file \ No newline at end of file
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.xml
new file mode 100644
index 000000000..d535d2c3f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist.xml
@@ -0,0 +1,304 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <gfxItems>
+ <gfxBlacklistEntry blockID="g35">
+ <os>WINNT 6.1</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.0</os>
+ <vendor>0xdcba</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry blockID="g36">
+ <os>WINNT 6.1</os>
+ <vendor>0xabab</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> GREATER_THAN_OR_EQUAL </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.1</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1111 </driverVersion>
+ <driverVersionComparator> EQUAL </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>abcd</vendor>
+ <devices>
+ <device>wxyz</device>
+ <device>asdf</device>
+ <device>erty</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 5 </driverVersion>
+ <driverVersionComparator> LESS_THAN_OR_EQUAL </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>dcdc</vendor>
+ <devices>
+ <device>uiop</device>
+ <device>vbnm</device>
+ <device>hjkl</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 5 </driverVersion>
+ <driverVersionComparator> EQUAL </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>abab</vendor>
+ <devices>
+ <device>ghjk</device>
+ <device>cvbn</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 7 </driverVersion>
+ <driverVersionComparator> GREATER_THAN_OR_EQUAL </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.1</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x6666</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DEVICE </featureStatus>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x6666</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DEVICE </featureStatus>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x6666</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DEVICE </featureStatus>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x6666</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DEVICE </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1112 </driverVersion>
+ <driverVersionMax> 8.52.323.1000 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_EXCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.50.322.1000 </driverVersion>
+ <driverVersionMax> 8.52.322.1112 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_EXCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1000 </driverVersion>
+ <driverVersionMax> 9.52.322.1000 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_EXCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> HARDWARE_VIDEO_DECODING </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 7.82.322.1000 </driverVersion>
+ <driverVersionMax> 9.25.322.1001 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1112 </driverVersion>
+ <driverVersionMax> 9.52.322.1300 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1000 </driverVersion>
+ <driverVersionMax> 8.52.322.1112 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1112 </driverVersion>
+ <driverVersionMax> 8.52.322.1200 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE_START </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_MSAA </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1000 </driverVersion>
+ <driverVersionMax> 8.52.322.1200 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE_START </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1000 </driverVersion>
+ <driverVersionMax> 8.52.322.1112 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_INCLUSIVE_START </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xdcdc</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> CANVAS2D_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.1000 </driverVersion>
+ <driverVersionMax> 9.52.322.1000 </driverVersionMax>
+ <driverVersionComparator> BETWEEN_EXCLUSIVE </driverVersionComparator>
+ </gfxBlacklistEntry>
+
+ </gfxItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist2.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist2.xml
new file mode 100644
index 000000000..0ad8f6819
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist2.xml
@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <gfxItems>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.1</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.0</os>
+ <vendor>0xdcba</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ </gfxItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml
new file mode 100755
index 000000000..f64676355
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_AllOS.xml
@@ -0,0 +1,783 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <gfxItems>
+
+ <gfxBlacklistEntry blockID="g1">
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g2">
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="22.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_LAYERS</feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1" maxVersion="22.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_1_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g11">
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="14.0b2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_OPENGL </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_MSAA </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> STAGEFRIGHT </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="17.2a2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="13.2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> HARDWARE_VIDEO_DECODING </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>All</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="10.5" maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g1">
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g2">
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="22.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_LAYERS</feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1" maxVersion="22.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_1_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g11">
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="14.0b2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_OPENGL </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_MSAA </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> STAGEFRIGHT </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="17.2a2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="13.2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> HARDWARE_VIDEO_DECODING </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="10.5" maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g1">
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g2">
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="22.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_LAYERS</feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1" maxVersion="22.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_1_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g11">
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="14.0b2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_OPENGL </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_MSAA </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> STAGEFRIGHT </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="17.2a2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="13.2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> HARDWARE_VIDEO_DECODING </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Linux</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="10.5" maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g1">
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g2">
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="22.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_9_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_LAYERS</feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="16.0a1" maxVersion="22.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_10_1_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry blockID="g11">
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="14.0b2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_OPENGL </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="12.0" maxVersion="16.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBGL_MSAA </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> STAGEFRIGHT </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="42.0" maxVersion="13.0b2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="17.2a2" maxVersion="15.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="15.0" maxVersion="13.2"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> HARDWARE_VIDEO_DECODING </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ <gfxBlacklistEntry>
+ <os>Android</os>
+ <vendor>0xabcd</vendor>
+ <versionRange minVersion="10.5" maxVersion="13.0"/>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT3D_11_ANGLE </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ </gfxBlacklistEntry>
+
+ </gfxItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.xml
new file mode 100644
index 000000000..248868a2e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_gfxBlacklist_OSVersion.xml
@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <gfxItems>
+ <gfxBlacklistEntry>
+ <os>WINNT 6.2</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> DIRECT2D </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ <gfxBlacklistEntry>
+ <os>Darwin 13</os>
+ <vendor>0xabcd</vendor>
+ <devices>
+ <device>0x2783</device>
+ <device>0x1234</device>
+ <device>0x2782</device>
+ </devices>
+ <feature> OPENGL_LAYERS </feature>
+ <featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
+ <driverVersion> 8.52.322.2202 </driverVersion>
+ <driverVersionComparator> LESS_THAN </driverVersionComparator>
+ </gfxBlacklistEntry>
+ </gfxItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf
new file mode 100644
index 000000000..016726021
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_1.rdf
@@ -0,0 +1,26 @@
+<?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:extension:hotfix@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_hotfix_1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf
new file mode 100644
index 000000000..35a2befee
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_2.rdf
@@ -0,0 +1,26 @@
+<?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:extension:hotfix@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_hotfix_2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf
new file mode 100644
index 000000000..7180da143
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_hotfix_3.rdf
@@ -0,0 +1,26 @@
+<?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:extension:hotfix@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>3.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_hotfix_3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_install.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_install.rdf
new file mode 100644
index 000000000..fe82334fa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_install.rdf
@@ -0,0 +1,63 @@
+<?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:extension:addon3@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon4@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon7@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>5.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_install.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_install.xml
new file mode 100644
index 000000000..33f14a2fd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_install.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1">
+ <addon>
+ <name>Real Test 2</name>
+ <type id='1'>Extension</type>
+ <guid>addon2@tests.mozilla.org</guid>
+ <version>1.0</version>
+ <authors>
+ <author>
+ <name>Test Creator</name>
+ <link>http://example.com/creator.html</link>
+ </author>
+ </authors>
+ <status id='4'>Public</status>
+ <summary>Repository summary</summary>
+ <description>Repository description</description>
+ <compatible_applications>
+ <application>
+ <name>Firefox</name>
+ <appID>{ec8030f7-c20a-464f-9b0e-13a3a9e97384}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ <application>
+ <name>SeaMonkey</name>
+ <appID>{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}</appID>
+ <min_version>0</min_version>
+ <max_version>*</max_version>
+ </application>
+ </compatible_applications>
+ <compatible_os>ALL</compatible_os>
+ <install size="2">http://example.com/browser/toolkit/mozapps/extensions/test/browser/addons/browser_install1_2.xpi</install>
+ </addon>
+
+ <addon_compatibility hosted="false">
+ <guid>addon6@tests.mozilla.org</guid>
+ <name>Addon Test 6</name>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>1.0</min_version>
+ <max_version>1.0</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <min_version>1.0</min_version>
+ <max_version>1.0</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate.rdf
new file mode 100644
index 000000000..d1dc992d5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate.rdf
@@ -0,0 +1,125 @@
+<?xml version="1.0"?>
+
+<!-- This is a copy of extensions.rdf from Firefox 3.5 including four
+ test extensions. Addon1 was user enabled, addon2 was user disabled, addon3
+ was pending user disable at the next restart and addon4 was pending user
+ enable at the next restart. Additionally addon1 and 2 have had
+ compatibility updates applies to make them compatible with the app and
+ toolkit respectively, addon3 and 4 have not. addon5 is disabled however
+ at the same time as the migration a new version should be detected. addon6
+ is pending install and needs a compatibility update to be compatible.
+ It also contains two themes in the profile -->
+
+<RDF:RDF xmlns:NS1="http://www.mozilla.org/2004/em-rdf#"
+ xmlns:NC="http://home.netscape.com/NC-rdf#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#">
+ <RDF:Description RDF:about="rdf:#$w8dNC3"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="1" />
+ <RDF:Description RDF:about="rdf:#$w8dNC4"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$w8dNC5"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$w8dNC6"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$w8dNC2"
+ NS1:id="toolkit@mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="1" />
+ <RDF:Description RDF:about="rdf:#$w8dNC1"
+ NS1:id="toolkit@mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$w8dNC7"
+ NS1:id="toolkit@mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$oadNC1"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:minVersion="1"
+ NS1:maxVersion="2" />
+ <RDF:Description RDF:about="rdf:#$TpnM4"
+ NS1:id="xpcshell@tests.mozilla.org"
+ NS1:updatedMinVersion="1"
+ NS1:updatedMaxVersion="2" />
+ <RDF:Description RDF:about="urn:mozilla:item:addon1@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="1.0"
+ NS1:name="Test 1">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$oadNC1"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:addon2@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="2.0"
+ NS1:name="Test 2"
+ NS1:userDisabled="true">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC1"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:addon3@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="2.0"
+ NS1:name="Test 3"
+ NS1:userDisabled="needs-disable">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC3"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:addon4@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="2.0"
+ NS1:name="Test 4"
+ NS1:userDisabled="needs-enable">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC2"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:addon5@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="1.0"
+ NS1:name="Test 5"
+ NS1:userDisabled="true">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC7"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:addon6@tests.mozilla.org"
+ NS1:name="Test 6"
+ NS1:version="1.0"
+ NS1:newVersion="1.0"
+ NS1:installLocation="app-profile">
+ <NS1:type NC:parseType="Integer">2</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$TpnM4"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:theme1@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="1.0"
+ NS1:name="Theme 2"
+ NS1:internalName="theme1/1.0">
+ <NS1:type NC:parseType="Integer">4</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC5"/>
+ </RDF:Description>
+ <RDF:Description RDF:about="urn:mozilla:item:theme2@tests.mozilla.org"
+ NS1:installLocation="app-profile"
+ NS1:version="2.0"
+ NS1:name="Theme 2"
+ NS1:internalName="theme2/1.0">
+ <NS1:type NC:parseType="Integer">4</NS1:type>
+ <NS1:targetApplication RDF:resource="rdf:#$w8dNC6"/>
+ </RDF:Description>
+ <RDF:Seq RDF:about="urn:mozilla:item:root">
+ <RDF:li RDF:resource="urn:mozilla:item:addon1@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:addon2@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:addon3@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:addon4@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:addon5@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:addon6@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:theme1@tests.mozilla.org"/>
+ <RDF:li RDF:resource="urn:mozilla:item:theme2@tests.mozilla.org"/>
+ </RDF:Seq>
+</RDF:RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate4.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate4.rdf
new file mode 100644
index 000000000..a3bf4f8ae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_migrate4.rdf
@@ -0,0 +1,46 @@
+<?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:extension:addon5@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon6@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_migrate4_6.xpi</em:updateLink>
+ <em:updateInfoURL>http://example.com/updateInfo.xhtml</em:updateInfoURL>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json
new file mode 100644
index 000000000..2773c7f98
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_no_update.json
@@ -0,0 +1,7 @@
+{
+ "addons": {
+ "test_no_update_webext@tests.mozilla.org": {
+ "updates": []
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
new file mode 100644
index 000000000..699257f87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/ancient.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem blockID="i454" id="ancient@tests.mozilla.org">
+ <versionRange minVersion="0" maxVersion="*" severity="3"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
new file mode 100644
index 000000000..8cbfb5d6a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/new.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1396046918000">
+ <emItems>
+ <emItem blockID="i454" id="new@tests.mozilla.org">
+ <versionRange minVersion="0" maxVersion="*" severity="3"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
new file mode 100644
index 000000000..75bd6e934
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_overrideblocklist/old.xml
@@ -0,0 +1,8 @@
+<?xml version="1.0"?>
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist" lastupdate="1296046918000">
+ <emItems>
+ <emItem blockID="i454" id="old@tests.mozilla.org">
+ <versionRange minVersion="0" maxVersion="*" severity="3"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml
new file mode 100644
index 000000000..d3564aebd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtp.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^test_plugin_0"/>
+ <versionRange minVersion="0" maxVersion="*" severity="0" vulnerabilitystatus="0"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_plugin_1"/>
+ <versionRange minVersion="0" maxVersion="*" severity="0" vulnerabilitystatus="1"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_plugin_2"/>
+ <versionRange minVersion="0" maxVersion="*" severity="0" vulnerabilitystatus="2"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_plugin_3"/>
+ <versionRange minVersion="0" maxVersion="*" vulnerabilitystatus="2"/>
+ </pluginItem>
+ <pluginItem>
+ <match name="name" exp="^test_plugin_4"/>
+ <versionRange minVersion="0" maxVersion="*" severity="1" vulnerabilitystatus="2"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml
new file mode 100644
index 000000000..7cd8496b3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_pluginBlocklistCtpUndo.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <pluginItems>
+ <pluginItem>
+ <match name="name" exp="^Test Plug-in"/>
+ <versionRange minVersion="0" maxVersion="*" severity="0" vulnerabilitystatus="2"/>
+ </pluginItem>
+ </pluginItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_proxy/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_proxy/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_proxy/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml
new file mode 100644
index 000000000..a1d18470c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_softblocked1.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<blocklist xmlns="http://www.mozilla.org/2006/addons-blocklist">
+ <emItems>
+ <emItem id="softblock1@tests.mozilla.org">
+ <versionRange severity="1"/>
+ </emItem>
+ </emItems>
+</blocklist>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml
new file mode 100644
index 000000000..949288e3f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_sourceURI.xml
@@ -0,0 +1,18 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="1">
+ <!-- Passes all requirements -->
+ <addon>
+ <name>Test</name>
+ <type id="1">Extension</type>
+ <guid>addon@tests.mozilla.org</guid>
+ <version>1</version>
+ <compatible_applications>
+ <application>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ </application>
+ </compatible_applications>
+ <install>http://www.example.com/testaddon.xpi</install>
+ </addon>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_temporary/bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/data/test_temporary/bootstrap.js
new file mode 100644
index 000000000..1666f2972
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_temporary/bootstrap.js
@@ -0,0 +1 @@
+Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json
new file mode 100644
index 000000000..027a9b233
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.json
@@ -0,0 +1,215 @@
+{
+ "addons": {
+ "addon1@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_min_version": "1"
+ }
+ }
+ },
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "2",
+ "strict_min_version": "2"
+ }
+ }
+ },
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update.xpi",
+ "update_info_url": "http://example.com/updateInfo.xhtml",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_min_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon2@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon3@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "3",
+ "advisory_max_version": "3"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon4@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "5.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "0"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon7@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0",
+ "advisory_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon8@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update8.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "advisory_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon9@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update9_2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "advisory_max_version": "1"
+ }
+ }
+ },
+ {
+ "_comment_": "Incompatible when strict compatibility is enabled",
+ "version": "3.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update9_3.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.9",
+ "advisory_max_version": "0.9"
+ }
+ }
+ },
+ {
+ "_comment_": "Incompatible due to compatibility override",
+ "version": "4.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update9_4.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.9",
+ "advisory_max_version": "0.9"
+ }
+ }
+ },
+ {
+ "_comment_": "Addon for future version of app",
+ "version": "4.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update9_5.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "5",
+ "advisory_max_version": "6"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon10@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update10.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.1",
+ "advisory_max_version": "0.4"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon11@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update11.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.1",
+ "strict_max_version": "0.2"
+ }
+ }
+ }
+ ]
+ },
+
+ "addon12@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:%PORT%/addons/test_update12.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "advisory_max_version": "1"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.rdf
new file mode 100644
index 000000000..4d4640f60
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.rdf
@@ -0,0 +1,270 @@
+<?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:extension:addon1@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <!-- Shouldn't fire onCompatibilityUpdateAvailable since this
+ information is already in the install.rdf -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Should be ignored as it is not for the present version of the
+ application -->
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update.xpi</em:updateLink>
+ <em:updateInfoURL>http://example.com/updateInfo.xhtml</em:updateInfoURL>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon2@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon3@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>3</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon4@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>5.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>0</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon7@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon8@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update8.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon9@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update9_2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Incompatible when strict compatibility is enabled -->
+ <li>
+ <Description>
+ <em:version>3.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.9</em:minVersion>
+ <em:maxVersion>0.9</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update9_3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Incompatible due to compatibility override -->
+ <li>
+ <Description>
+ <em:version>4.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.9</em:minVersion>
+ <em:maxVersion>0.9</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update9_4.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Addon for future version of app -->
+ <li>
+ <Description>
+ <em:version>5.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>5</em:minVersion>
+ <em:maxVersion>6</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update9_5.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon10@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.1</em:minVersion>
+ <em:maxVersion>0.4</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update10.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:extension:addon11@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.1</em:minVersion>
+ <em:maxVersion>0.2</em:maxVersion>
+ <em:strictCompatibility>true</em:strictCompatibility>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update11.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <Description about="urn:mozilla:item:addon12@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:%PORT%/addons/test_update12.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update.xml b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.xml
new file mode 100644
index 000000000..62928815b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update.xml
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="utf-8" ?>
+<searchresults total_results="11">
+ <addon>
+ <name>Test Addon 9</name>
+ <type id="1">Extension</type>
+ <guid>addon9@tests.mozilla.org</guid>
+ </addon>
+ <addon_compatibility hosted="true">
+ <guid>addon9@tests.mozilla.org</guid>
+ <name>Test Addon 9</name>
+ <version_ranges>
+ <version_range type="incompatible">
+ <min_version>4</min_version>
+ <max_version>4</max_version>
+ <compatible_applications>
+ <application>
+ <name>XPCShell</name>
+ <min_version>1</min_version>
+ <max_version>1</max_version>
+ <appID>xpcshell@tests.mozilla.org</appID>
+ </application>
+ </compatible_applications>
+ </version_range>
+ </version_ranges>
+ </addon_compatibility>
+</searchresults>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_update_multi.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_multi.rdf
new file mode 100644
index 000000000..f28a3f26d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_update_multi.rdf
@@ -0,0 +1,26 @@
+<?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:extension:updatemulti@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/addons/test_update_multi2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json
new file mode 100644
index 000000000..811e50158
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.json
@@ -0,0 +1,327 @@
+{
+ "addons": {
+ "updatecheck1@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "1.0",
+ "update_link": "https://localhost:4444/addons/test1.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ },
+ {
+ "_comment_": "This update is incompatible and so should not be considered a valid update",
+ "version": "2.0",
+ "update_link": "https://localhost:4444/addons/test2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "2",
+ "strict_max_version": "2"
+ }
+ }
+ },
+ {
+ "version": "3.0",
+ "update_link": "https://localhost:4444/addons/test3.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ },
+ {
+ "version": "2.0",
+ "update_link": "https://localhost:4444/addons/test2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "2"
+ }
+ }
+ },
+ {
+ "_comment_": "This update is incompatible and so should not be considered a valid update",
+ "version": "4.0",
+ "update_link": "https://localhost:4444/addons/test4.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "2",
+ "strict_max_version": "2"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_5@tests.mozilla.org": {
+ "_comment_": "An update which expects a signature. It will fail since signatures are ",
+ "_comment_": "supported in this format.",
+ "_comment_": "The updateLink will also be ignored since it is not secure and there ",
+ "_comment_": "is no updateHash.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_5@tests.mozilla.org": {
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_7@tests.mozilla.org": {
+ "_comment_": "An update which expects a signature. It will fail since signatures are ",
+ "_comment_": "supported in this format.",
+ "_comment_": "The updateLink will also be ignored since it is not secure ",
+ "_comment_": "and there is no updateHash.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "2"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_8@tests.mozilla.org": {
+ "_comment_": "The updateLink will be ignored since it is not secure and ",
+ "_comment_": "there is no updateHash.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_9@tests.mozilla.org": {
+ "_comment_": "The updateLink will used since there is an updateHash to verify it.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "update_hash": "sha256:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_10@tests.mozilla.org": {
+ "_comment_": "The updateLink will used since it is a secure URL.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_11@tests.mozilla.org": {
+ "_comment_": "The updateLink will used since it is a secure URL.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://localhost:4444/broken.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_12@tests.mozilla.org": {
+ "_comment_": "The updateLink will not be used since the updateHash ",
+ "_comment_": "verifying it is not strong enough.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "http://localhost:4444/broken.xpi",
+ "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "test_bug378216_13@tests.mozilla.org": {
+ "_comment_": "An update with a weak hash. The updateLink will used since it is ",
+ "_comment_": "a secure URL.",
+
+ "updates": [
+ {
+ "version": "2.0",
+ "update_link": "https://localhost:4444/broken.xpi",
+ "update_hash": "sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "1",
+ "strict_max_version": "1"
+ }
+ }
+ }
+ ]
+ },
+
+ "_comment_": "There should be no information present for test_bug378216_14",
+
+ "test_bug378216_15@tests.mozilla.org": {
+ "_comment_": "Invalid update JSON",
+
+ "updates": "foo"
+ },
+
+ "ignore-compat@tests.mozilla.org": {
+ "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled",
+
+ "updates": [
+ {
+ "version": "1.0",
+ "update_link": "https://localhost:4444/addons/test1.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.1",
+ "advisory_max_version": "0.2"
+ }
+ }
+ },
+ {
+ "version": "2.0",
+ "update_link": "https://localhost:4444/addons/test2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.5",
+ "advisory_max_version": "0.6"
+ }
+ }
+ },
+ {
+ "_comment_": "Update for future app versions - should never be compatible",
+ "version": "3.0",
+ "update_link": "https://localhost:4444/addons/test3.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "2",
+ "advisory_max_version": "3"
+ }
+ }
+ }
+ ]
+ },
+
+ "compat-override@tests.mozilla.org": {
+ "_comment_": "Various updates available - one is not compatible, but compatibility checking is disabled",
+
+ "updates": [
+ {
+ "_comment_": "Has compatibility override, but it doesn't match this app version",
+ "version": "1.0",
+ "update_link": "https://localhost:4444/addons/test1.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.1",
+ "advisory_max_version": "0.2"
+ }
+ }
+ },
+ {
+ "_comment_": "Has compatibility override, so is incompaible",
+ "version": "2.0",
+ "update_link": "https://localhost:4444/addons/test2.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.5",
+ "advisory_max_version": "0.6"
+ }
+ }
+ },
+ {
+ "_comment_": "Update for future app versions - should never be compatible",
+ "version": "3.0",
+ "update_link": "https://localhost:4444/addons/test3.xpi",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "2",
+ "advisory_max_version": "3"
+ }
+ }
+ }
+ ]
+ },
+
+ "compat-strict-optin@tests.mozilla.org": {
+ "_comment_": "Opt-in to strict compatibility checking",
+
+ "updates": [
+ {
+ "version": "1.0",
+ "update_link": "https://localhost:4444/addons/test1.xpi",
+ "_comment_": "strictCompatibility: true",
+ "applications": {
+ "gecko": {
+ "strict_min_version": "0.1",
+ "strict_max_version": "0.2"
+ }
+ }
+ }
+ ]
+ }
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf
new file mode 100644
index 000000000..c5d97ada0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecheck.rdf
@@ -0,0 +1,419 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:updatecheck1@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- This update is incompatible and so should not be considered a valid
+ update -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <li>
+ <Description>
+ <em:version>3.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- This update is incompatible and so should not be considered a valid
+ update -->
+ <li>
+ <Description>
+ <em:version>4.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test4.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <!-- An update with no signature which will fail if retrieved with an update
+ key. The updateLink will also be ignored since it is not secure and there
+ is no updateHash. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_5@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ </RDF:Description>
+
+ <!-- An update with a broken signature which will fail if retrieved with an
+ update key. The updateLink will also be ignored since it is not secure
+ and there is no updateHash. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_7@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBAMO1O2gwSCCth1GwYMgscfaNakpN40PJfOWt
+ ub2HVdg8+OXMciF8d/9eVWm8eH/IxuxyZlmRZTs3O5tv9eWAY5uBCtqDf1WgTsGk
+ jrgZow1fITkZI7w0//C8eKdMLAtGueGfNs2IlTd5P/0KH/hf1rPc1wUqEqKCd4+L
+ BcVq13ad</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will be ignored since it
+ is not secure and there is no updateHash. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_8@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBAMH/33P/bn148mVkAB8i5X8c4LhY52E+MPUT
+ yKHGpowZnRLgL2O0dfpm+rljOBfKi51322PFrsc6VIFml6x4Lrb5foxSyB0Vs9pb
+ SEDFWUKquOQvceQ9iEx5Pe0VzrmUZgcQxd8ksSunWL4wJaBZ/evE5amFC6sw3pv/
+ fjt8p3GN</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will used since there is
+ an updateHash to verify it. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_9@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ <em:updateHash>sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBAJ5Dv3Zd7/j5dLchCw9iO/cxPq8oOhOYD2M+
+ jUKvmHCyTBRIEaJrE4N7yVbRYk++ERIfyVepLivsVi4pBmF7JTdw0NaKUA0LiOoT
+ mRL8I7s5NPjCiiNcdqbncWyiZwIj1w1nkbWGTlH/gEjRW/LbvT4JAuec8yNFDa4S
+ X8mOMf7k</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will used since it is
+ a secure URL. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_10@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>https://localhost:4444/broken.xpi</em:updateLink>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBAGvf7XqqoTl5WofrNq55E7W+UttOEDXLB3Oi
+ XDiXe0i6njlozilseaUo1hgfQhhzN9gkyetP5tGBVcLRrVyliKpJmD6ABCVGW1lS
+ qS+SEw7gDHyHkvwKMyWKedpRGChqLYnnf+Y+CX3MWLZLkwPXMKdTYgN3Rx0lEnJk
+ 37LSEMKE</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will used since it is
+ a secure URL. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_11@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>https://localhost:4444/broken.xpi</em:updateLink>
+ <em:updateHash>sha1:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBACMX/KReOGSJ8CMGRroH1v3Gjv/Qs/pqH+Ow
+ o+hCKWLUKx7hpJgVJkXXdAHW0U88NXlp1S2H0WqA7I/CdmNXJSPzzV/J4z1dZgXh
+ JbW6mqNb0pj6nIe7g8OLzSxDgBmO4DUP5DAmnmqciJLWQzN7OdbcwrWz6xPN5kZF
+ A90eF5zy</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will not be used since the
+ updateHash verifying it is not strong enough. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_12@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/broken.xpi</em:updateLink>
+ <em:updateHash>md2:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBAJRfcFvHIWxVyycCw8IjNmEhabc2uqA1zQwp
+ 5oKh3Y23hwIsQ6xy68Wtjte1NEYFRt5fWkbMXj9YQj6LpVbzBKiGATcrq6MycZKK
+ o5N22cWbrKKRweJezTyN4eLfQg21pG7r8mdfS0bIA28ZVFtQOmORejoUesEouCGy
+ eKYk9nS2</em:signature>
+ </RDF:Description>
+
+ <!-- An update with a valid signature. The updateLink will used since it is
+ a secure URL. -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_13@tests.mozilla.org">
+ <em:updates>
+ <RDF:Seq>
+ <RDF:li>
+ <RDF:Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <RDF:Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>https://localhost:4444/broken.xpi</em:updateLink>
+ <em:updateHash>md2:78fc1d2887eda35b4ad2e3a0b60120ca271ce6e6</em:updateHash>
+ </RDF:Description>
+ </em:targetApplication>
+ </RDF:Description>
+ </RDF:li>
+ </RDF:Seq>
+ </em:updates>
+ <em:signature>MIGTMA0GCSqGSIb3DQEBBQUAA4GBALQKwzLFr/VOw3gJvv/LCh3/PWDd9FqmFnX+
+ hJjBmCaUDtG7CXn1i0h8ed8IeRHpLLT7FCzVwU3bH9BUjdm8wc3ObtlNbd8go01a
+ CoXz50r3rYPcYz4WS+7/+lvrUqsuWd9Wj+q0NeCPiNaaro6/AolE2Qf5JFRL3lxY
+ lsKWAnVO</em:signature>
+ </RDF:Description>
+
+ <!-- There should be no information present for test_bug378216_14 -->
+
+ <!-- Invalid update RDF -->
+ <RDF:Description about="urn:mozilla:extension:test_bug378216_15@tests.mozilla.org">
+ <em:updates>Foo</em:updates>
+ </RDF:Description>
+
+ <!-- Various updates available - one is not compatible, but compatibility checking is disabled -->
+ <Description about="urn:mozilla:extension:ignore-compat@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.1</em:minVersion>
+ <em:maxVersion>0.2</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.5</em:minVersion>
+ <em:maxVersion>0.6</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Update for future app versions - should never be compatible -->
+ <li>
+ <Description>
+ <em:version>3.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <!-- Various updates available - one is not compatible, but compatibility checking is disabled -->
+ <Description about="urn:mozilla:extension:compat-override@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <!-- Has compatibility override, but it doesn't match this app version -->
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.1</em:minVersion>
+ <em:maxVersion>0.2</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Has compatibility override, so is incompaible -->
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.5</em:minVersion>
+ <em:maxVersion>0.6</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ <!-- Update for future app versions - should never be compatible -->
+ <li>
+ <Description>
+ <em:version>3.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>2</em:minVersion>
+ <em:maxVersion>3</em:maxVersion>
+ <em:updateLink>https://localhost:4444/addons/test3.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+ <!-- Opt-in to strict compatibility checking -->
+ <Description about="urn:mozilla:extension:compat-strict-optin@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>1.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>0.1</em:minVersion>
+ <em:maxVersion>0.2</em:maxVersion>
+ <em:strictCompatibility>true</em:strictCompatibility>
+ <em:updateLink>https://localhost:4444/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.rdf
new file mode 100644
index 000000000..ec6e88ec4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_ignore.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:compatmode-ignore@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:%PORT%/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.rdf
new file mode 100644
index 000000000..2ef88860e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_normal.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:compatmode-normal@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:%PORT%/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.rdf
new file mode 100644
index 000000000..2f72c181d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updatecompatmode_strict.rdf
@@ -0,0 +1,26 @@
+<?xml version="1.0" encoding="UTF-8"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:RDF="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:extension:compatmode-strict@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>2</em:maxVersion>
+ <em:updateLink>https://localhost:%PORT%/addons/test1.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/test_updateid.rdf b/toolkit/mozapps/extensions/test/xpcshell/data/test_updateid.rdf
new file mode 100644
index 000000000..c13928520
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/test_updateid.rdf
@@ -0,0 +1,26 @@
+<?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:extension:addon1@tests.mozilla.org">
+ <em:updates>
+ <Seq>
+ <li>
+ <Description>
+ <em:version>2.0</em:version>
+ <em:targetApplication>
+ <Description>
+ <em:id>xpcshell@tests.mozilla.org</em:id>
+ <em:minVersion>1</em:minVersion>
+ <em:maxVersion>1</em:maxVersion>
+ <em:updateLink>http://localhost:4444/addons/test_updateid2.xpi</em:updateLink>
+ </Description>
+ </em:targetApplication>
+ </Description>
+ </li>
+ </Seq>
+ </em:updates>
+ </Description>
+
+</RDF>
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi
new file mode 100644
index 000000000..51b00475a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
new file mode 100644
index 000000000..6b4abaa69
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/data/webext-implicit-id.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
new file mode 100644
index 000000000..960caceeb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -0,0 +1,1345 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var AM_Cc = Components.classes;
+var AM_Ci = Components.interfaces;
+var AM_Cu = Components.utils;
+
+AM_Cu.importGlobalProperties(["TextEncoder"]);
+
+const CERTDB_CONTRACTID = "@mozilla.org/security/x509certdb;1";
+const CERTDB_CID = Components.ID("{fb0bbc5c-452e-4783-b32c-80124693d871}");
+
+const PREF_EM_CHECK_UPDATE_SECURITY = "extensions.checkUpdateSecurity";
+const PREF_EM_STRICT_COMPATIBILITY = "extensions.strictCompatibility";
+const PREF_EM_MIN_COMPAT_APP_VERSION = "extensions.minCompatibleAppVersion";
+const PREF_EM_MIN_COMPAT_PLATFORM_VERSION = "extensions.minCompatiblePlatformVersion";
+const PREF_GETADDONS_BYIDS = "extensions.getAddons.get.url";
+const PREF_GETADDONS_BYIDS_PERFORMANCE = "extensions.getAddons.getWithPerformance.url";
+const PREF_XPI_SIGNATURES_REQUIRED = "xpinstall.signatures.required";
+
+// Forcibly end the test if it runs longer than 15 minutes
+const TIMEOUT_MS = 900000;
+
+// Maximum error in file modification times. Some file systems don't store
+// modification times exactly. As long as we are closer than this then it
+// still passes.
+const MAX_TIME_DIFFERENCE = 3000;
+
+// Time to reset file modified time relative to Date.now() so we can test that
+// times are modified (10 hours old).
+const MAKE_FILE_OLD_DIFFERENCE = 10 * 3600 * 1000;
+
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+Components.utils.import("resource://gre/modules/Task.jsm");
+const { OS } = Components.utils.import("resource://gre/modules/osfile.jsm", {});
+Components.utils.import("resource://gre/modules/AsyncShutdown.jsm");
+
+Components.utils.import("resource://testing-common/AddonTestUtils.jsm");
+
+XPCOMUtils.defineLazyModuleGetter(this, "Extension",
+ "resource://gre/modules/Extension.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "ExtensionTestUtils",
+ "resource://testing-common/ExtensionXPCShellUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "HttpServer",
+ "resource://testing-common/httpd.js");
+XPCOMUtils.defineLazyModuleGetter(this, "MockAsyncShutdown",
+ "resource://testing-common/AddonTestUtils.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistrar",
+ "resource://testing-common/MockRegistrar.jsm");
+XPCOMUtils.defineLazyModuleGetter(this, "MockRegistry",
+ "resource://testing-common/MockRegistry.jsm");
+
+const {
+ awaitPromise,
+ createAppInfo,
+ createInstallRDF,
+ createTempWebExtensionFile,
+ createUpdateRDF,
+ getFileForAddon,
+ manuallyInstall,
+ manuallyUninstall,
+ promiseAddonByID,
+ promiseAddonEvent,
+ promiseAddonsByIDs,
+ promiseAddonsWithOperationsByTypes,
+ promiseCompleteAllInstalls,
+ promiseConsoleOutput,
+ promiseFindAddonUpdates,
+ promiseInstallAllFiles,
+ promiseInstallFile,
+ promiseRestartManager,
+ promiseSetExtensionModifiedTime,
+ promiseShutdownManager,
+ promiseStartupManager,
+ promiseWriteProxyFileToDir,
+ registerDirectory,
+ setExtensionModifiedTime,
+ writeFilesToZip
+} = AddonTestUtils;
+
+// WebExtension wrapper for ease of testing
+ExtensionTestUtils.init(this);
+
+AddonTestUtils.init(this);
+AddonTestUtils.overrideCertDB();
+
+Object.defineProperty(this, "gAppInfo", {
+ get() {
+ return AddonTestUtils.appInfo;
+ },
+});
+
+Object.defineProperty(this, "gExtensionsINI", {
+ get() {
+ return AddonTestUtils.extensionsINI.clone();
+ },
+});
+
+Object.defineProperty(this, "gInternalManager", {
+ get() {
+ return AddonTestUtils.addonIntegrationService.QueryInterface(AM_Ci.nsITimerCallback);
+ },
+});
+
+Object.defineProperty(this, "gProfD", {
+ get() {
+ return AddonTestUtils.profileDir.clone();
+ },
+});
+
+Object.defineProperty(this, "gTmpD", {
+ get() {
+ return AddonTestUtils.tempDir.clone();
+ },
+});
+
+Object.defineProperty(this, "gUseRealCertChecks", {
+ get() {
+ return AddonTestUtils.useRealCertChecks;
+ },
+ set(val) {
+ return AddonTestUtils.useRealCertChecks = val;
+ },
+});
+
+Object.defineProperty(this, "TEST_UNPACKED", {
+ get() {
+ return AddonTestUtils.testUnpacked;
+ },
+ set(val) {
+ return AddonTestUtils.testUnpacked = val;
+ },
+});
+
+// We need some internal bits of AddonManager
+var AMscope = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
+var { AddonManager, AddonManagerInternal, AddonManagerPrivate } = AMscope;
+
+var gPort = null;
+var gUrlToFileMap = {};
+
+// Map resource://xpcshell-data/ to the data directory
+var resHandler = Services.io.getProtocolHandler("resource")
+ .QueryInterface(AM_Ci.nsISubstitutingProtocolHandler);
+// Allow non-existent files because of bug 1207735
+var dataURI = NetUtil.newURI(do_get_file("data", true));
+resHandler.setSubstitution("xpcshell-data", dataURI);
+
+function isManifestRegistered(file) {
+ let manifests = Components.manager.getManifestLocations();
+ for (let i = 0; i < manifests.length; i++) {
+ let manifest = manifests.queryElementAt(i, AM_Ci.nsIURI);
+
+ // manifest is the url to the manifest file either in an XPI or a directory.
+ // We want the location of the XPI or directory itself.
+ if (manifest instanceof AM_Ci.nsIJARURI) {
+ manifest = manifest.JARFile.QueryInterface(AM_Ci.nsIFileURL).file;
+ }
+ else if (manifest instanceof AM_Ci.nsIFileURL) {
+ manifest = manifest.file.parent;
+ }
+ else {
+ continue;
+ }
+
+ if (manifest.equals(file))
+ return true;
+ }
+ return false;
+}
+
+// Listens to messages from bootstrap.js telling us what add-ons were started
+// and stopped etc. and performs some sanity checks that only installed add-ons
+// are started etc.
+this.BootstrapMonitor = {
+ inited: false,
+
+ // Contain the current state of add-ons in the system
+ installed: new Map(),
+ started: new Map(),
+
+ // Contain the last state of shutdown and uninstall calls for an add-on
+ stopped: new Map(),
+ uninstalled: new Map(),
+
+ startupPromises: [],
+ installPromises: [],
+
+ init() {
+ this.inited = true;
+ Services.obs.addObserver(this, "bootstrapmonitor-event", false);
+ },
+
+ shutdownCheck() {
+ if (!this.inited)
+ return;
+
+ do_check_eq(this.started.size, 0);
+ },
+
+ clear(id) {
+ this.installed.delete(id);
+ this.started.delete(id);
+ this.stopped.delete(id);
+ this.uninstalled.delete(id);
+ },
+
+ promiseAddonStartup(id) {
+ return new Promise(resolve => {
+ this.startupPromises.push(resolve);
+ });
+ },
+
+ promiseAddonInstall(id) {
+ return new Promise(resolve => {
+ this.installPromises.push(resolve);
+ });
+ },
+
+ checkMatches(cached, current) {
+ do_check_neq(cached, undefined);
+ do_check_eq(current.data.version, cached.data.version);
+ do_check_eq(current.data.installPath, cached.data.installPath);
+ do_check_eq(current.data.resourceURI, cached.data.resourceURI);
+ },
+
+ checkAddonStarted(id, version = undefined) {
+ let started = this.started.get(id);
+ do_check_neq(started, undefined);
+ if (version != undefined)
+ do_check_eq(started.data.version, version);
+
+ // Chrome should be registered by now
+ let installPath = new FileUtils.File(started.data.installPath);
+ let isRegistered = isManifestRegistered(installPath);
+ do_check_true(isRegistered);
+ },
+
+ checkAddonNotStarted(id) {
+ do_check_false(this.started.has(id));
+ },
+
+ checkAddonInstalled(id, version = undefined) {
+ const installed = this.installed.get(id);
+ notEqual(installed, undefined);
+ if (version !== undefined) {
+ equal(installed.data.version, version);
+ }
+ return installed;
+ },
+
+ checkAddonNotInstalled(id) {
+ do_check_false(this.installed.has(id));
+ },
+
+ observe(subject, topic, data) {
+ let info = JSON.parse(data);
+ let id = info.data.id;
+ let installPath = new FileUtils.File(info.data.installPath);
+
+ if (subject && subject.wrappedJSObject) {
+ // NOTE: in some of the new tests, we need to received the real objects instead of
+ // their JSON representations, but most of the current tests expect intallPath
+ // and resourceURI to have been converted to strings.
+ info.data = Object.assign({}, subject.wrappedJSObject.data, {
+ installPath: info.data.installPath,
+ resourceURI: info.data.resourceURI,
+ });
+ }
+
+ // If this is the install event the add-ons shouldn't already be installed
+ if (info.event == "install") {
+ this.checkAddonNotInstalled(id);
+
+ this.installed.set(id, info);
+
+ for (let resolve of this.installPromises)
+ resolve();
+ this.installPromises = [];
+ }
+ else {
+ this.checkMatches(this.installed.get(id), info);
+ }
+
+ // If this is the shutdown event than the add-on should already be started
+ if (info.event == "shutdown") {
+ this.checkMatches(this.started.get(id), info);
+
+ this.started.delete(id);
+ this.stopped.set(id, info);
+
+ // Chrome should still be registered at this point
+ let isRegistered = isManifestRegistered(installPath);
+ do_check_true(isRegistered);
+
+ // XPIProvider doesn't bother unregistering chrome on app shutdown but
+ // since we simulate restarts we must do so manually to keep the registry
+ // consistent.
+ if (info.reason == 2 /* APP_SHUTDOWN */)
+ Components.manager.removeBootstrappedManifestLocation(installPath);
+ }
+ else {
+ this.checkAddonNotStarted(id);
+ }
+
+ if (info.event == "uninstall") {
+ // Chrome should be unregistered at this point
+ let isRegistered = isManifestRegistered(installPath);
+ do_check_false(isRegistered);
+
+ this.installed.delete(id);
+ this.uninstalled.set(id, info)
+ }
+ else if (info.event == "startup") {
+ this.started.set(id, info);
+
+ // Chrome should be registered at this point
+ let isRegistered = isManifestRegistered(installPath);
+ do_check_true(isRegistered);
+
+ for (let resolve of this.startupPromises)
+ resolve();
+ this.startupPromises = [];
+ }
+ }
+}
+
+AddonTestUtils.on("addon-manager-shutdown", () => BootstrapMonitor.shutdownCheck());
+
+function isNightlyChannel() {
+ var channel = "default";
+ try {
+ channel = Services.prefs.getCharPref("app.update.channel");
+ }
+ catch (e) { }
+
+ return channel != "aurora" && channel != "beta" && channel != "release" && channel != "esr";
+}
+
+/**
+ * Tests that an add-on does appear in the crash report annotations, if
+ * crash reporting is enabled. The test will fail if the add-on is not in the
+ * annotation.
+ * @param aId
+ * The ID of the add-on
+ * @param aVersion
+ * The version of the add-on
+ */
+function do_check_in_crash_annotation(aId, aVersion) {
+ if (!("nsICrashReporter" in AM_Ci))
+ return;
+
+ if (!("Add-ons" in gAppInfo.annotations)) {
+ do_check_false(true);
+ return;
+ }
+
+ let addons = gAppInfo.annotations["Add-ons"].split(",");
+ do_check_false(addons.indexOf(encodeURIComponent(aId) + ":" +
+ encodeURIComponent(aVersion)) < 0);
+}
+
+/**
+ * Tests that an add-on does not appear in the crash report annotations, if
+ * crash reporting is enabled. The test will fail if the add-on is in the
+ * annotation.
+ * @param aId
+ * The ID of the add-on
+ * @param aVersion
+ * The version of the add-on
+ */
+function do_check_not_in_crash_annotation(aId, aVersion) {
+ if (!("nsICrashReporter" in AM_Ci))
+ return;
+
+ if (!("Add-ons" in gAppInfo.annotations)) {
+ do_check_true(true);
+ return;
+ }
+
+ let addons = gAppInfo.annotations["Add-ons"].split(",");
+ do_check_true(addons.indexOf(encodeURIComponent(aId) + ":" +
+ encodeURIComponent(aVersion)) < 0);
+}
+
+/**
+ * Returns a testcase xpi
+ *
+ * @param aName
+ * The name of the testcase (without extension)
+ * @return an nsIFile pointing to the testcase xpi
+ */
+function do_get_addon(aName) {
+ return do_get_file("addons/" + aName + ".xpi");
+}
+
+function do_get_addon_hash(aName, aAlgorithm) {
+ let file = do_get_addon(aName);
+ return do_get_file_hash(file);
+}
+
+function do_get_file_hash(aFile, aAlgorithm) {
+ if (!aAlgorithm)
+ aAlgorithm = "sha1";
+
+ let crypto = AM_Cc["@mozilla.org/security/hash;1"].
+ createInstance(AM_Ci.nsICryptoHash);
+ crypto.initWithString(aAlgorithm);
+ let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ crypto.updateFromStream(fis, aFile.fileSize);
+
+ // return the two-digit hexadecimal code for a byte
+ let toHexString = charCode => ("0" + charCode.toString(16)).slice(-2);
+
+ let binary = crypto.finish(false);
+ let hash = Array.from(binary, c => toHexString(c.charCodeAt(0)));
+ return aAlgorithm + ":" + hash.join("");
+}
+
+/**
+ * Returns an extension uri spec
+ *
+ * @param aProfileDir
+ * The extension install directory
+ * @return a uri spec pointing to the root of the extension
+ */
+function do_get_addon_root_uri(aProfileDir, aId) {
+ let path = aProfileDir.clone();
+ path.append(aId);
+ if (!path.exists()) {
+ path.leafName += ".xpi";
+ return "jar:" + Services.io.newFileURI(path).spec + "!/";
+ }
+ return Services.io.newFileURI(path).spec;
+}
+
+function do_get_expected_addon_name(aId) {
+ if (TEST_UNPACKED)
+ return aId;
+ return aId + ".xpi";
+}
+
+/**
+ * Check that an array of actual add-ons is the same as an array of
+ * expected add-ons.
+ *
+ * @param aActualAddons
+ * The array of actual add-ons to check.
+ * @param aExpectedAddons
+ * The array of expected add-ons to check against.
+ * @param aProperties
+ * An array of properties to check.
+ */
+function do_check_addons(aActualAddons, aExpectedAddons, aProperties) {
+ do_check_neq(aActualAddons, null);
+ do_check_eq(aActualAddons.length, aExpectedAddons.length);
+ for (let i = 0; i < aActualAddons.length; i++)
+ do_check_addon(aActualAddons[i], aExpectedAddons[i], aProperties);
+}
+
+/**
+ * Check that the actual add-on is the same as the expected add-on.
+ *
+ * @param aActualAddon
+ * The actual add-on to check.
+ * @param aExpectedAddon
+ * The expected add-on to check against.
+ * @param aProperties
+ * An array of properties to check.
+ */
+function do_check_addon(aActualAddon, aExpectedAddon, aProperties) {
+ do_check_neq(aActualAddon, null);
+
+ aProperties.forEach(function(aProperty) {
+ let actualValue = aActualAddon[aProperty];
+ let expectedValue = aExpectedAddon[aProperty];
+
+ // Check that all undefined expected properties are null on actual add-on
+ if (!(aProperty in aExpectedAddon)) {
+ if (actualValue !== undefined && actualValue !== null) {
+ do_throw("Unexpected defined/non-null property for add-on " +
+ aExpectedAddon.id + " (addon[" + aProperty + "] = " +
+ actualValue.toSource() + ")");
+ }
+
+ return;
+ }
+ else if (expectedValue && !actualValue) {
+ do_throw("Missing property for add-on " + aExpectedAddon.id +
+ ": expected addon[" + aProperty + "] = " + expectedValue);
+ return;
+ }
+
+ switch (aProperty) {
+ case "creator":
+ do_check_author(actualValue, expectedValue);
+ break;
+
+ case "developers":
+ case "translators":
+ case "contributors":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_author(actualValue[i], expectedValue[i]);
+ break;
+
+ case "screenshots":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_screenshot(actualValue[i], expectedValue[i]);
+ break;
+
+ case "sourceURI":
+ do_check_eq(actualValue.spec, expectedValue);
+ break;
+
+ case "updateDate":
+ do_check_eq(actualValue.getTime(), expectedValue.getTime());
+ break;
+
+ case "compatibilityOverrides":
+ do_check_eq(actualValue.length, expectedValue.length);
+ for (let i = 0; i < actualValue.length; i++)
+ do_check_compatibilityoverride(actualValue[i], expectedValue[i]);
+ break;
+
+ case "icons":
+ do_check_icons(actualValue, expectedValue);
+ break;
+
+ default:
+ if (remove_port(actualValue) !== remove_port(expectedValue))
+ do_throw("Failed for " + aProperty + " for add-on " + aExpectedAddon.id +
+ " (" + actualValue + " === " + expectedValue + ")");
+ }
+ });
+}
+
+/**
+ * Check that the actual author is the same as the expected author.
+ *
+ * @param aActual
+ * The actual author to check.
+ * @param aExpected
+ * The expected author to check against.
+ */
+function do_check_author(aActual, aExpected) {
+ do_check_eq(aActual.toString(), aExpected.name);
+ do_check_eq(aActual.name, aExpected.name);
+ do_check_eq(aActual.url, aExpected.url);
+}
+
+/**
+ * Check that the actual screenshot is the same as the expected screenshot.
+ *
+ * @param aActual
+ * The actual screenshot to check.
+ * @param aExpected
+ * The expected screenshot to check against.
+ */
+function do_check_screenshot(aActual, aExpected) {
+ do_check_eq(aActual.toString(), aExpected.url);
+ do_check_eq(aActual.url, aExpected.url);
+ do_check_eq(aActual.width, aExpected.width);
+ do_check_eq(aActual.height, aExpected.height);
+ do_check_eq(aActual.thumbnailURL, aExpected.thumbnailURL);
+ do_check_eq(aActual.thumbnailWidth, aExpected.thumbnailWidth);
+ do_check_eq(aActual.thumbnailHeight, aExpected.thumbnailHeight);
+ do_check_eq(aActual.caption, aExpected.caption);
+}
+
+/**
+ * Check that the actual compatibility override is the same as the expected
+ * compatibility override.
+ *
+ * @param aAction
+ * The actual compatibility override to check.
+ * @param aExpected
+ * The expected compatibility override to check against.
+ */
+function do_check_compatibilityoverride(aActual, aExpected) {
+ do_check_eq(aActual.type, aExpected.type);
+ do_check_eq(aActual.minVersion, aExpected.minVersion);
+ do_check_eq(aActual.maxVersion, aExpected.maxVersion);
+ do_check_eq(aActual.appID, aExpected.appID);
+ do_check_eq(aActual.appMinVersion, aExpected.appMinVersion);
+ do_check_eq(aActual.appMaxVersion, aExpected.appMaxVersion);
+}
+
+function do_check_icons(aActual, aExpected) {
+ for (var size in aExpected) {
+ do_check_eq(remove_port(aActual[size]), remove_port(aExpected[size]));
+ }
+}
+
+function startupManager(aAppChanged) {
+ promiseStartupManager(aAppChanged);
+}
+
+/**
+ * Restarts the add-on manager as if the host application was restarted.
+ *
+ * @param aNewVersion
+ * An optional new version to use for the application. Passing this
+ * will change nsIXULAppInfo.version and make the startup appear as if
+ * the application version has changed.
+ */
+function restartManager(aNewVersion) {
+ awaitPromise(promiseRestartManager(aNewVersion));
+}
+
+function shutdownManager() {
+ awaitPromise(promiseShutdownManager());
+}
+
+function isItemMarkedMPIncompatible(aId) {
+ return AddonTestUtils.addonsList.isMultiprocessIncompatible(aId);
+}
+
+function isThemeInAddonsList(aDir, aId) {
+ return AddonTestUtils.addonsList.hasTheme(aDir, aId);
+}
+
+function isExtensionInAddonsList(aDir, aId) {
+ return AddonTestUtils.addonsList.hasExtension(aDir, aId);
+}
+
+function check_startup_changes(aType, aIds) {
+ var ids = aIds.slice(0);
+ ids.sort();
+ var changes = AddonManager.getStartupChanges(aType);
+ changes = changes.filter(aEl => /@tests.mozilla.org$/.test(aEl));
+ changes.sort();
+
+ do_check_eq(JSON.stringify(ids), JSON.stringify(changes));
+}
+
+/**
+ * Writes an install.rdf manifest into a directory using the properties passed
+ * in a JS object. The objects should contain a property for each property to
+ * appear in the RDF. The object may contain an array of objects with id,
+ * minVersion and maxVersion in the targetApplications property to give target
+ * application compatibility.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @param aDir
+ * The directory to add the install.rdf to
+ * @param aId
+ * An optional string to override the default installation aId
+ * @param aExtraFile
+ * An optional dummy file to create in the directory
+ * @return An nsIFile for the directory in which the add-on is installed.
+ */
+function writeInstallRDFToDir(aData, aDir, aId = aData.id, aExtraFile = null) {
+ let files = {
+ "install.rdf": AddonTestUtils.createInstallRDF(aData),
+ };
+ if (aExtraFile)
+ files[aExtraFile] = "";
+
+ let dir = aDir.clone();
+ dir.append(aId);
+
+ awaitPromise(AddonTestUtils.promiseWriteFilesToDir(dir.path, files));
+ return dir;
+}
+
+/**
+ * Writes an install.rdf manifest into a packed extension using the properties passed
+ * in a JS object. The objects should contain a property for each property to
+ * appear in the RDF. The object may contain an array of objects with id,
+ * minVersion and maxVersion in the targetApplications property to give target
+ * application compatibility.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @param aDir
+ * The install directory to add the extension to
+ * @param aId
+ * An optional string to override the default installation aId
+ * @param aExtraFile
+ * An optional dummy file to create in the extension
+ * @return A file pointing to where the extension was installed
+ */
+function writeInstallRDFToXPI(aData, aDir, aId = aData.id, aExtraFile = null) {
+ let files = {
+ "install.rdf": AddonTestUtils.createInstallRDF(aData),
+ };
+ if (aExtraFile)
+ files[aExtraFile] = "";
+
+ if (!aDir.exists())
+ aDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ var file = aDir.clone();
+ file.append(`${aId}.xpi`);
+
+ AddonTestUtils.writeFilesToZip(file.path, files);
+
+ return file;
+}
+
+/**
+ * Writes an install.rdf manifest into an extension using the properties passed
+ * in a JS object. The objects should contain a property for each property to
+ * appear in the RDF. The object may contain an array of objects with id,
+ * minVersion and maxVersion in the targetApplications property to give target
+ * application compatibility.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @param aDir
+ * The install directory to add the extension to
+ * @param aId
+ * An optional string to override the default installation aId
+ * @param aExtraFile
+ * An optional dummy file to create in the extension
+ * @return A file pointing to where the extension was installed
+ */
+function writeInstallRDFForExtension(aData, aDir, aId, aExtraFile) {
+ if (TEST_UNPACKED) {
+ return writeInstallRDFToDir(aData, aDir, aId, aExtraFile);
+ }
+ return writeInstallRDFToXPI(aData, aDir, aId, aExtraFile);
+}
+
+/**
+ * Writes a manifest.json manifest into an extension using the properties passed
+ * in a JS object.
+ *
+ * @param aManifest
+ * The data to write
+ * @param aDir
+ * The install directory to add the extension to
+ * @param aId
+ * An optional string to override the default installation aId
+ * @return A file pointing to where the extension was installed
+ */
+function promiseWriteWebManifestForExtension(aData, aDir, aId = aData.applications.gecko.id) {
+ let files = {
+ "manifest.json": JSON.stringify(aData),
+ }
+ return AddonTestUtils.promiseWriteFilesToExtension(aDir.path, aId, files);
+}
+
+/**
+ * Creates an XPI file for some manifest data in the temporary directory and
+ * returns the nsIFile for it. The file will be deleted when the test completes.
+ *
+ * @param aData
+ * The object holding data about the add-on
+ * @return A file pointing to the created XPI file
+ */
+function createTempXPIFile(aData, aExtraFile) {
+ let files = {
+ "install.rdf": aData,
+ };
+ if (typeof aExtraFile == "object")
+ Object.assign(files, aExtraFile);
+ else if (aExtraFile)
+ files[aExtraFile] = "";
+
+ return AddonTestUtils.createTempXPIFile(files);
+}
+
+var gExpectedEvents = {};
+var gExpectedInstalls = [];
+var gNext = null;
+
+function getExpectedEvent(aId) {
+ if (!(aId in gExpectedEvents))
+ do_throw("Wasn't expecting events for " + aId);
+ if (gExpectedEvents[aId].length == 0)
+ do_throw("Too many events for " + aId);
+ let event = gExpectedEvents[aId].shift();
+ if (event instanceof Array)
+ return event;
+ return [event, true];
+}
+
+function getExpectedInstall(aAddon) {
+ if (gExpectedInstalls instanceof Array)
+ return gExpectedInstalls.shift();
+ if (!aAddon || !aAddon.id)
+ return gExpectedInstalls["NO_ID"].shift();
+ let id = aAddon.id;
+ if (!(id in gExpectedInstalls) || !(gExpectedInstalls[id] instanceof Array))
+ do_throw("Wasn't expecting events for " + id);
+ if (gExpectedInstalls[id].length == 0)
+ do_throw("Too many events for " + id);
+ return gExpectedInstalls[id].shift();
+}
+
+const AddonListener = {
+ onPropertyChanged: function(aAddon, aProperties) {
+ do_print(`Got onPropertyChanged event for ${aAddon.id}`);
+ let [event, properties] = getExpectedEvent(aAddon.id);
+ do_check_eq("onPropertyChanged", event);
+ do_check_eq(aProperties.length, properties.length);
+ properties.forEach(function(aProperty) {
+ // Only test that the expected properties are listed, having additional
+ // properties listed is not necessary a problem
+ if (aProperties.indexOf(aProperty) == -1)
+ do_throw("Did not see property change for " + aProperty);
+ });
+ return check_test_completed(arguments);
+ },
+
+ onEnabling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onEnabling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onEnabling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE));
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ return check_test_completed(arguments);
+ },
+
+ onEnabled: function(aAddon) {
+ do_print(`Got onEnabled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onEnabled", event);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ return check_test_completed(arguments);
+ },
+
+ onDisabling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onDisabling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onDisabling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE));
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ return check_test_completed(arguments);
+ },
+
+ onDisabled: function(aAddon) {
+ do_print(`Got onDisabled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onDisabled", event);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ return check_test_completed(arguments);
+ },
+
+ onInstalling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onInstalling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onInstalling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_INSTALL));
+ return check_test_completed(arguments);
+ },
+
+ onInstalled: function(aAddon) {
+ do_print(`Got onInstalled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onInstalled", event);
+ return check_test_completed(arguments);
+ },
+
+ onUninstalling: function(aAddon, aRequiresRestart) {
+ do_print(`Got onUninstalling event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onUninstalling", event);
+ do_check_eq(aRequiresRestart, expectedRestart);
+ if (expectedRestart)
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL));
+ return check_test_completed(arguments);
+ },
+
+ onUninstalled: function(aAddon) {
+ do_print(`Got onUninstalled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onUninstalled", event);
+ return check_test_completed(arguments);
+ },
+
+ onOperationCancelled: function(aAddon) {
+ do_print(`Got onOperationCancelled event for ${aAddon.id}`);
+ let [event, expectedRestart] = getExpectedEvent(aAddon.id);
+ do_check_eq("onOperationCancelled", event);
+ return check_test_completed(arguments);
+ }
+};
+
+const InstallListener = {
+ onNewInstall: function(install) {
+ if (install.state != AddonManager.STATE_DOWNLOADED &&
+ install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
+ install.state != AddonManager.STATE_AVAILABLE)
+ do_throw("Bad install state " + install.state);
+ if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
+ do_check_eq(install.error, 0);
+ else
+ do_check_neq(install.error, 0);
+ do_check_eq("onNewInstall", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadStarted: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADING);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadStarted", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadEnded: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadEnded", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadFailed: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq("onDownloadFailed", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onDownloadCancelled: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_CANCELLED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onDownloadCancelled", getExpectedInstall());
+ return check_test_completed(arguments);
+ },
+
+ onInstallStarted: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALLING);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallStarted", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallEnded: function(install, newAddon) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallEnded", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallFailed: function(install) {
+ do_check_eq(install.state, AddonManager.STATE_INSTALL_FAILED);
+ do_check_eq("onInstallFailed", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onInstallCancelled: function(install) {
+ // If the install was cancelled by a listener returning false from
+ // onInstallStarted, then the state will revert to STATE_DOWNLOADED.
+ let possibleStates = [AddonManager.STATE_CANCELLED,
+ AddonManager.STATE_DOWNLOADED];
+ do_check_true(possibleStates.indexOf(install.state) != -1);
+ do_check_eq(install.error, 0);
+ do_check_eq("onInstallCancelled", getExpectedInstall(install.addon));
+ return check_test_completed(arguments);
+ },
+
+ onExternalInstall: function(aAddon, existingAddon, aRequiresRestart) {
+ do_check_eq("onExternalInstall", getExpectedInstall(aAddon));
+ do_check_false(aRequiresRestart);
+ return check_test_completed(arguments);
+ }
+};
+
+function hasFlag(aBits, aFlag) {
+ return (aBits & aFlag) != 0;
+}
+
+// Just a wrapper around setting the expected events
+function prepare_test(aExpectedEvents, aExpectedInstalls, aNext) {
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ gExpectedInstalls = aExpectedInstalls;
+ gExpectedEvents = aExpectedEvents;
+ gNext = aNext;
+}
+
+// Checks if all expected events have been seen and if so calls the callback
+function check_test_completed(aArgs) {
+ if (!gNext)
+ return undefined;
+
+ if (gExpectedInstalls instanceof Array &&
+ gExpectedInstalls.length > 0)
+ return undefined;
+
+ for (let id in gExpectedInstalls) {
+ let installList = gExpectedInstalls[id];
+ if (installList.length > 0)
+ return undefined;
+ }
+
+ for (let id in gExpectedEvents) {
+ if (gExpectedEvents[id].length > 0)
+ return undefined;
+ }
+
+ return gNext.apply(null, aArgs);
+}
+
+// Verifies that all the expected events for all add-ons were seen
+function ensure_test_completed() {
+ for (let i in gExpectedEvents) {
+ if (gExpectedEvents[i].length > 0)
+ do_throw("Didn't see all the expected events for " + i);
+ }
+ gExpectedEvents = {};
+ if (gExpectedInstalls)
+ do_check_eq(gExpectedInstalls.length, 0);
+}
+
+/**
+ * A helper method to install an array of AddonInstall to completion and then
+ * call a provided callback.
+ *
+ * @param aInstalls
+ * The array of AddonInstalls to install
+ * @param aCallback
+ * The callback to call when all installs have finished
+ */
+function completeAllInstalls(aInstalls, aCallback) {
+ promiseCompleteAllInstalls(aInstalls).then(aCallback);
+}
+
+/**
+ * A helper method to install an array of files and call a callback after the
+ * installs are completed.
+ *
+ * @param aFiles
+ * The array of files to install
+ * @param aCallback
+ * The callback to call when all installs have finished
+ * @param aIgnoreIncompatible
+ * Optional parameter to ignore add-ons that are incompatible in
+ * aome way with the application
+ */
+function installAllFiles(aFiles, aCallback, aIgnoreIncompatible) {
+ promiseInstallAllFiles(aFiles, aIgnoreIncompatible).then(aCallback);
+}
+
+const EXTENSIONS_DB = "extensions.json";
+var gExtensionsJSON = gProfD.clone();
+gExtensionsJSON.append(EXTENSIONS_DB);
+
+
+// By default use strict compatibility
+Services.prefs.setBoolPref("extensions.strictCompatibility", true);
+
+// By default, set min compatible versions to 0
+Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0");
+Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_PLATFORM_VERSION, "0");
+
+// Ensure signature checks are enabled by default
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+
+
+// Copies blocklistFile (an nsIFile) to gProfD/blocklist.xml.
+function copyBlocklistToProfile(blocklistFile) {
+ var dest = gProfD.clone();
+ dest.append("blocklist.xml");
+ if (dest.exists())
+ dest.remove(false);
+ blocklistFile.copyTo(gProfD, "blocklist.xml");
+ dest.lastModifiedTime = Date.now();
+}
+
+// Throw a failure and attempt to abandon the test if it looks like it is going
+// to timeout
+function timeout() {
+ timer = null;
+ do_throw("Test ran longer than " + TIMEOUT_MS + "ms");
+
+ // Attempt to bail out of the test
+ do_test_finished();
+}
+
+var timer = AM_Cc["@mozilla.org/timer;1"].createInstance(AM_Ci.nsITimer);
+timer.init(timeout, TIMEOUT_MS, AM_Ci.nsITimer.TYPE_ONE_SHOT);
+
+// Make sure that a given path does not exist
+function pathShouldntExist(file) {
+ if (file.exists()) {
+ do_throw(`Test cleanup: path ${file.path} exists when it should not`);
+ }
+}
+
+do_register_cleanup(function addon_cleanup() {
+ if (timer)
+ timer.cancel();
+});
+
+/**
+ * Creates a new HttpServer for testing, and begins listening on the
+ * specified port. Automatically shuts down the server when the test
+ * unit ends.
+ *
+ * @param port
+ * The port to listen on. If omitted, listen on a random
+ * port. The latter is the preferred behavior.
+ *
+ * @return HttpServer
+ */
+function createHttpServer(port = -1) {
+ let server = new HttpServer();
+ server.start(port);
+
+ do_register_cleanup(() => {
+ return new Promise(resolve => {
+ server.stop(resolve);
+ });
+ });
+
+ return server;
+}
+
+/**
+ * Handler function that responds with the interpolated
+ * static file associated to the URL specified by request.path.
+ * This replaces the %PORT% entries in the file with the actual
+ * value of the running server's port (stored in gPort).
+ */
+function interpolateAndServeFile(request, response) {
+ try {
+ let file = gUrlToFileMap[request.path];
+ var data = "";
+ var fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ var cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ fstream.init(file, -1, 0, 0);
+ cstream.init(fstream, "UTF-8", 0, 0);
+
+ let str = {};
+ let read = 0;
+ do {
+ // read as much as we can and put it in str.value
+ read = cstream.readString(0xffffffff, str);
+ data += str.value;
+ } while (read != 0);
+ data = data.replace(/%PORT%/g, gPort);
+
+ response.write(data);
+ } catch (e) {
+ do_throw(`Exception while serving interpolated file: ${e}\n${e.stack}`);
+ } finally {
+ cstream.close(); // this closes fstream as well
+ }
+}
+
+/**
+ * Sets up a path handler for the given URL and saves the
+ * corresponding file in the global url -> file map.
+ *
+ * @param url
+ * the actual URL
+ * @param file
+ * nsILocalFile representing a static file
+ */
+function mapUrlToFile(url, file, server) {
+ server.registerPathHandler(url, interpolateAndServeFile);
+ gUrlToFileMap[url] = file;
+}
+
+function mapFile(path, server) {
+ mapUrlToFile(path, do_get_file(path), server);
+}
+
+/**
+ * Take out the port number in an URL
+ *
+ * @param url
+ * String that represents an URL with a port number in it
+ */
+function remove_port(url) {
+ if (typeof url === "string")
+ return url.replace(/:\d+/, "");
+ return url;
+}
+// Wrap a function (typically a callback) to catch and report exceptions
+function do_exception_wrap(func) {
+ return function() {
+ try {
+ func.apply(null, arguments);
+ }
+ catch (e) {
+ do_report_unexpected_exception(e);
+ }
+ };
+}
+
+/**
+ * Change the schema version of the JSON extensions database
+ */
+function changeXPIDBVersion(aNewVersion, aMutator = undefined) {
+ let jData = loadJSON(gExtensionsJSON);
+ jData.schemaVersion = aNewVersion;
+ if (aMutator)
+ aMutator(jData);
+ saveJSON(jData, gExtensionsJSON);
+}
+
+/**
+ * Load a file into a string
+ */
+function loadFile(aFile) {
+ let data = "";
+ let fstream = Components.classes["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Components.interfaces.nsIFileInputStream);
+ let cstream = Components.classes["@mozilla.org/intl/converter-input-stream;1"].
+ createInstance(Components.interfaces.nsIConverterInputStream);
+ fstream.init(aFile, -1, 0, 0);
+ 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);
+ cstream.close();
+ return data;
+}
+
+/**
+ * Raw load of a JSON file
+ */
+function loadJSON(aFile) {
+ let data = loadFile(aFile);
+ do_print("Loaded JSON file " + aFile.path);
+ return (JSON.parse(data));
+}
+
+/**
+ * Raw save of a JSON blob to file
+ */
+function saveJSON(aData, aFile) {
+ do_print("Starting to save JSON file " + aFile.path);
+ let stream = FileUtils.openSafeFileOutputStream(aFile);
+ let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(AM_Ci.nsIConverterOutputStream);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ // XXX pretty print the JSON while debugging
+ converter.writeString(JSON.stringify(aData, null, 2));
+ converter.flush();
+ // nsConverterOutputStream doesn't finish() safe output streams on close()
+ FileUtils.closeSafeFileOutputStream(stream);
+ converter.close();
+ do_print("Done saving JSON file " + aFile.path);
+}
+
+/**
+ * Create a callback function that calls do_execute_soon on an actual callback and arguments
+ */
+function callback_soon(aFunction) {
+ return function(...args) {
+ do_execute_soon(function() {
+ aFunction.apply(null, args);
+ }, aFunction.name ? "delayed callback " + aFunction.name : "delayed callback");
+ }
+}
+
+function writeProxyFileToDir(aDir, aAddon, aId) {
+ awaitPromise(promiseWriteProxyFileToDir(aDir, aAddon, aId));
+
+ let file = aDir.clone();
+ file.append(aId);
+ return file
+}
+
+function* serveSystemUpdate(xml, perform_update, testserver) {
+ testserver.registerPathHandler("/data/update.xml", (request, response) => {
+ response.write(xml);
+ });
+
+ try {
+ yield perform_update();
+ }
+ finally {
+ testserver.registerPathHandler("/data/update.xml", null);
+ }
+}
+
+// Runs an update check making it use the passed in xml string. Uses the direct
+// call to the update function so we get rejections on failure.
+function* installSystemAddons(xml, testserver) {
+ do_print("Triggering system add-on update check.");
+
+ yield serveSystemUpdate(xml, function*() {
+ let { XPIProvider } = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", {});
+ yield XPIProvider.updateSystemAddons();
+ }, testserver);
+}
+
+// Runs a full add-on update check which will in some cases do a system add-on
+// update check. Always succeeds.
+function* updateAllSystemAddons(xml, testserver) {
+ do_print("Triggering full add-on update check.");
+
+ yield serveSystemUpdate(xml, function() {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+ resolve();
+ }, "addons-background-update-complete", false);
+
+ // Trigger the background update timer handler
+ gInternalManager.notify(null);
+ });
+ }, testserver);
+}
+
+// Builds an update.xml file for an update check based on the data passed.
+function* buildSystemAddonUpdates(addons, root) {
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>\n\n<updates>\n`;
+ if (addons) {
+ xml += ` <addons>\n`;
+ for (let addon of addons) {
+ xml += ` <addon id="${addon.id}" URL="${root + addon.path}" version="${addon.version}"`;
+ if (addon.size)
+ xml += ` size="${addon.size}"`;
+ if (addon.hashFunction)
+ xml += ` hashFunction="${addon.hashFunction}"`;
+ if (addon.hashValue)
+ xml += ` hashValue="${addon.hashValue}"`;
+ xml += `/>\n`;
+ }
+ xml += ` </addons>\n`;
+ }
+ xml += `</updates>\n`;
+
+ return xml;
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js b/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js
new file mode 100644
index 000000000..6310bbc60
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_unpack.js
@@ -0,0 +1,3 @@
+/* globals Services, TEST_UNPACKED: true*/
+Services.prefs.setBoolPref("extensions.alwaysUnpack", true);
+TEST_UNPACKED = true;
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
new file mode 100644
index 000000000..dd0dc1981
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository.js
@@ -0,0 +1,625 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests AddonRepository.jsm
+
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+
+const PREF_GETADDONS_BROWSEADDONS = "extensions.getAddons.browseAddons";
+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 PORT = gServer.identity.primaryPort;
+const BASE_URL = "http://localhost:" + PORT;
+const DEFAULT_URL = "about:blank";
+
+gPort = PORT;
+
+// Path to source URI of installed add-on
+const INSTALL_URL1 = "/addons/test_AddonRepository_1.xpi";
+// Path to source URI of installing add-on
+const INSTALL_URL2 = "/addons/test_AddonRepository_2.xpi";
+// Path to source URI of non-active add-on (state = STATE_AVAILABLE)
+const INSTALL_URL3 = "/addons/test_AddonRepository_3.xpi";
+
+// Properties of an individual add-on that should be checked
+// Note: name is checked separately
+var ADDON_PROPERTIES = ["id", "type", "version", "creator", "developers",
+ "description", "fullDescription", "developerComments",
+ "eula", "iconURL", "icons", "screenshots", "homepageURL",
+ "supportURL", "contributionURL", "contributionAmount",
+ "averageRating", "reviewCount", "reviewURL",
+ "totalDownloads", "weeklyDownloads", "dailyUsers",
+ "sourceURI", "repositoryStatus", "size", "updateDate",
+ "purchaseURL", "purchaseAmount", "purchaseDisplayAmount",
+ "compatibilityOverrides"];
+
+// Results of getAddonsByIDs
+var GET_RESULTS = [{
+ id: "test1@tests.mozilla.org",
+ type: "extension",
+ version: "1.1",
+ creator: {
+ name: "Test Creator 1",
+ url: BASE_URL + "/creator1.html"
+ },
+ developers: [{
+ name: "Test Developer 1",
+ url: BASE_URL + "/developer1.html"
+ }],
+ description: "Test Summary 1",
+ fullDescription: "Test Description 1",
+ developerComments: "Test Developer Comments 1",
+ eula: "Test EULA 1",
+ iconURL: BASE_URL + "/icon1.png",
+ icons: { "32": BASE_URL + "/icon1.png" },
+ screenshots: [{
+ url: BASE_URL + "/full1-1.png",
+ width: 400,
+ height: 300,
+ thumbnailURL: BASE_URL + "/thumbnail1-1.png",
+ thumbnailWidth: 200,
+ thumbnailHeight: 150,
+ caption: "Caption 1 - 1"
+ }, {
+ url: BASE_URL + "/full2-1.png",
+ thumbnailURL: BASE_URL + "/thumbnail2-1.png",
+ caption: "Caption 2 - 1"
+ }],
+ homepageURL: BASE_URL + "/learnmore1.html",
+ learnmoreURL: BASE_URL + "/learnmore1.html",
+ supportURL: BASE_URL + "/support1.html",
+ contributionURL: BASE_URL + "/meetDevelopers1.html",
+ contributionAmount: "$11.11",
+ averageRating: 4,
+ reviewCount: 1111,
+ reviewURL: BASE_URL + "/review1.html",
+ totalDownloads: 2222,
+ weeklyDownloads: 3333,
+ dailyUsers: 4444,
+ sourceURI: BASE_URL + INSTALL_URL2,
+ repositoryStatus: 8,
+ size: 5555,
+ updateDate: new Date(1265033045000),
+ compatibilityOverrides: [{
+ type: "incompatible",
+ minVersion: 0.1,
+ maxVersion: 0.2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 3.0,
+ appMaxVersion: 4.0
+ }, {
+ type: "incompatible",
+ minVersion: 0.2,
+ maxVersion: 0.3,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 5.0,
+ appMaxVersion: 6.0
+ }]
+}, {
+ id: "test_AddonRepository_1@tests.mozilla.org",
+ type: "theme",
+ version: "1.4",
+ repositoryStatus: 9999,
+ icons: {}
+}];
+
+// Results of retrieveRecommendedAddons and searchAddons
+var SEARCH_RESULTS = [{
+ id: "test1@tests.mozilla.org",
+ type: "extension",
+ version: "1.1",
+ creator: {
+ name: "Test Creator 1",
+ url: BASE_URL + "/creator1.html"
+ },
+ repositoryStatus: 8,
+ sourceURI: BASE_URL + "/test1.xpi",
+ icons: {}
+}, {
+ id: "test2@tests.mozilla.org",
+ type: "extension",
+ version: "1.2",
+ creator: {
+ name: "Test Creator 2",
+ url: BASE_URL + "/creator2.html"
+ },
+ developers: [{
+ name: "Test Developer 2",
+ url: BASE_URL + "/developer2.html"
+ }],
+ description: "Test Summary 2\n\nparagraph",
+ fullDescription: "Test Description 2\nnewline",
+ developerComments: "Test Developer\nComments 2",
+ eula: "Test EULA 2",
+ iconURL: BASE_URL + "/icon2-32.png",
+ icons: {
+ "32": BASE_URL + "/icon2-32.png",
+ "48": BASE_URL + "/icon2-48.png",
+ "64": BASE_URL + "/icon2-64.png"
+ },
+ screenshots: [{
+ url: BASE_URL + "/full1-2.png",
+ thumbnailURL: BASE_URL + "/thumbnail1-2.png"
+ }, {
+ url: BASE_URL + "/full2-2.png",
+ thumbnailURL: BASE_URL + "/thumbnail2-2.png",
+ caption: "Caption 2"
+ }],
+ homepageURL: BASE_URL + "/learnmore2.html",
+ supportURL: BASE_URL + "/support2.html",
+ learnmoreURL: BASE_URL + "/learnmore2.html",
+ contributionURL: BASE_URL + "/meetDevelopers2.html",
+ contributionAmount: null,
+ repositoryStatus: 4,
+ sourceURI: BASE_URL + "/test2.xpi"
+}, {
+ id: "test3@tests.mozilla.org",
+ type: "theme",
+ version: "1.3",
+ creator: {
+ name: "Test Creator 3",
+ url: BASE_URL + "/creator3.html"
+ },
+ developers: [{
+ name: "First Test Developer 3",
+ url: BASE_URL + "/developer1-3.html"
+ }, {
+ name: "Second Test Developer 3",
+ url: BASE_URL + "/developer2-3.html"
+ }],
+ description: "Test Summary 3",
+ fullDescription: "Test Description 3\n\n List item 1\n List item 2",
+ developerComments: "Test Developer Comments 3",
+ eula: "Test EULA 3",
+ iconURL: BASE_URL + "/icon3.png",
+ icons: { "32": BASE_URL + "/icon3.png" },
+ screenshots: [{
+ url: BASE_URL + "/full1-3.png",
+ thumbnailURL: BASE_URL + "/thumbnail1-3.png",
+ caption: "Caption 1 - 3"
+ }, {
+ url: BASE_URL + "/full2-3.png",
+ caption: "Caption 2 - 3"
+ }, {
+ url: BASE_URL + "/full3-3.png",
+ thumbnailURL: BASE_URL + "/thumbnail3-3.png",
+ caption: "Caption 3 - 3"
+ }],
+ homepageURL: BASE_URL + "/homepage3.html",
+ supportURL: BASE_URL + "/support3.html",
+ learnmoreURL: BASE_URL + "/learnmore3.html",
+ contributionURL: BASE_URL + "/meetDevelopers3.html",
+ contributionAmount: "$11.11",
+ averageRating: 2,
+ reviewCount: 1111,
+ reviewURL: BASE_URL + "/review3.html",
+ totalDownloads: 2222,
+ weeklyDownloads: 3333,
+ dailyUsers: 4444,
+ sourceURI: BASE_URL + "/test3.xpi",
+ repositoryStatus: 8,
+ size: 5555,
+ updateDate: new Date(1265033045000),
+
+}, {
+ id: "purchase1@tests.mozilla.org",
+ type: "extension",
+ version: "2.0",
+ creator: {
+ name: "Test Creator - Last Passing",
+ url: BASE_URL + "/creatorLastPassing.html"
+ },
+ averageRating: 5,
+ repositoryStatus: 4,
+ purchaseURL: "http://localhost:" + PORT + "/purchaseURL1",
+ purchaseAmount: 5,
+ purchaseDisplayAmount: "$5",
+ icons: {}
+}, {
+ id: "purchase2@tests.mozilla.org",
+ type: "extension",
+ version: "2.0",
+ creator: {
+ name: "Test Creator - Last Passing",
+ url: BASE_URL + "/creatorLastPassing.html"
+ },
+ averageRating: 5,
+ repositoryStatus: 4,
+ purchaseURL: "http://localhost:" + PORT + "/purchaseURL2",
+ purchaseAmount: 10,
+ purchaseDisplayAmount: "$10",
+ icons: {}
+}, {
+ id: "test-lastPassing@tests.mozilla.org",
+ type: "extension",
+ version: "2.0",
+ creator: {
+ name: "Test Creator - Last Passing",
+ url: BASE_URL + "/creatorLastPassing.html"
+ },
+ averageRating: 5,
+ repositoryStatus: 4,
+ sourceURI: BASE_URL + "/addons/test_AddonRepository_3.xpi",
+ icons: {}
+}];
+
+const TOTAL_RESULTS = 1111;
+const MAX_RESULTS = SEARCH_RESULTS.length;
+
+// Used to differentiate between testing that a search success
+// or a search failure for retrieveRecommendedAddons and searchAddons
+const FAILED_MAX_RESULTS = 9999;
+
+// Values for testing AddonRepository.getAddonsByIDs()
+var GET_TEST = {
+ preference: PREF_GETADDONS_BYIDS,
+ preferenceValue: BASE_URL + "/%OS%/%VERSION%/%API_VERSION%/" +
+ "%API_VERSION%/%IDS%",
+ failedIDs: ["test1@tests.mozilla.org"],
+ failedURL: "/XPCShell/1/1.5/1.5/test1%40tests.mozilla.org",
+ successfulIDs: ["test1@tests.mozilla.org",
+ "{00000000-1111-2222-3333-444444444444}",
+ "test_AddonRepository_1@tests.mozilla.org"],
+ successfulURL: "/XPCShell/1/1.5/1.5/test1%40tests.mozilla.org," +
+ "%7B00000000-1111-2222-3333-444444444444%7D," +
+ "test_AddonRepository_1%40tests.mozilla.org"
+};
+
+// Values for testing AddonRepository.retrieveRecommendedAddons()
+var RECOMMENDED_TEST = {
+ preference: PREF_GETADDONS_GETRECOMMENDED,
+ preferenceValue: BASE_URL + "/%OS%/%VERSION%/%API_VERSION%/" +
+ "%API_VERSION%/%MAX_RESULTS%",
+ failedURL: "/XPCShell/1/1.5/1.5/" + (2 * FAILED_MAX_RESULTS),
+ successfulURL: "/XPCShell/1/1.5/1.5/" + (2 * MAX_RESULTS)
+};
+
+// Values for testing AddonRepository.searchAddons()
+var SEARCH_TEST = {
+ searchTerms: "odd=search:with&weird\"characters",
+ preference: PREF_GETADDONS_GETSEARCHRESULTS,
+ preferenceValue: BASE_URL + "/%OS%/%VERSION%/%API_VERSION%/" +
+ "%API_VERSION%/%MAX_RESULTS%/%TERMS%",
+ failedURL: "/XPCShell/1/1.5/1.5/" + (2 * FAILED_MAX_RESULTS) +
+ "/odd%3Dsearch%3Awith%26weird%22characters",
+ successfulURL: "/XPCShell/1/1.5/1.5/" + (2 * MAX_RESULTS) +
+ "/odd%3Dsearch%3Awith%26weird%22characters"
+};
+
+// Test that actual results and expected results are equal
+function check_results(aActualAddons, aExpectedAddons, aAddonCount, aInstallNull) {
+ do_check_false(AddonRepository.isSearching);
+
+ do_check_eq(aActualAddons.length, aAddonCount);
+ do_check_addons(aActualAddons, aExpectedAddons, ADDON_PROPERTIES);
+
+ // Additional tests
+ aActualAddons.forEach(function check_each_addon(aActualAddon) {
+ // Separately check name so better messages are output when test fails
+ if (aActualAddon.name == "FAIL")
+ do_throw(aActualAddon.id + " - " + aActualAddon.description);
+ if (aActualAddon.name != "PASS")
+ do_throw(aActualAddon.id + " - " + "invalid add-on name " + aActualAddon.name);
+
+ do_check_eq(aActualAddon.install == null, !!aInstallNull || !aActualAddon.sourceURI);
+
+ // Check that sourceURI property consistent within actual addon
+ if (aActualAddon.install)
+ do_check_eq(aActualAddon.install.sourceURI.spec, aActualAddon.sourceURI.spec);
+ });
+}
+
+// Complete a search, also testing cancelSearch() and isSearching
+function complete_search(aSearch, aSearchCallback) {
+ var failCallback = {
+ searchSucceeded: function(addons, length, total) {
+ do_throw("failCallback.searchSucceeded should not be called");
+ end_test();
+ },
+
+ searchFailed: function() {
+ do_throw("failCallback.searchFailed should not be called");
+ end_test();
+ }
+ };
+
+ var callbackCalled = false;
+ var testCallback = {
+ searchSucceeded: function(addons, length, total) {
+ do_throw("testCallback.searchSucceeded should not be called");
+ end_test();
+ },
+
+ searchFailed: function() {
+ callbackCalled = true;
+ }
+ };
+
+ // Should fail because cancelled it immediately
+ aSearch(failCallback);
+ do_check_true(AddonRepository.isSearching);
+ AddonRepository.cancelSearch();
+ do_check_false(AddonRepository.isSearching);
+
+ aSearch(aSearchCallback);
+ do_check_true(AddonRepository.isSearching);
+
+ // searchFailed should be called immediately because already searching
+ aSearch(testCallback);
+ do_check_true(callbackCalled);
+ do_check_true(AddonRepository.isSearching);
+}
+
+
+function run_test() {
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ startupManager();
+
+ // Install an add-on so can check that it isn't returned in the results
+ installAllFiles([do_get_addon("test_AddonRepository_1")], function addon_1_install_callback() {
+ restartManager();
+
+ // Register other add-on XPI files
+ gServer.registerFile(INSTALL_URL2,
+ do_get_addon("test_AddonRepository_2"));
+ gServer.registerFile(INSTALL_URL3,
+ do_get_addon("test_AddonRepository_3"));
+
+ // Register files used to test search failure
+ mapUrlToFile(GET_TEST.failedURL,
+ do_get_file("data/test_AddonRepository_failed.xml"),
+ gServer);
+ mapUrlToFile(RECOMMENDED_TEST.failedURL,
+ do_get_file("data/test_AddonRepository_failed.xml"),
+ gServer);
+ mapUrlToFile(SEARCH_TEST.failedURL,
+ do_get_file("data/test_AddonRepository_failed.xml"),
+ gServer);
+
+ // Register files used to test search success
+ mapUrlToFile(GET_TEST.successfulURL,
+ do_get_file("data/test_AddonRepository_getAddonsByIDs.xml"),
+ gServer);
+ mapUrlToFile(RECOMMENDED_TEST.successfulURL,
+ do_get_file("data/test_AddonRepository.xml"),
+ gServer);
+ mapUrlToFile(SEARCH_TEST.successfulURL,
+ do_get_file("data/test_AddonRepository.xml"),
+ gServer);
+
+ // Create an active AddonInstall so can check that it isn't returned in the results
+ AddonManager.getInstallForURL(BASE_URL + INSTALL_URL2, function addon_2_get(aInstall) {
+ try {
+ aInstall.install();
+ }
+ catch (e) {
+ do_print("Failed to install add-on " + aInstall.sourceURI.spec);
+ do_report_unexpected_exception(e);
+ }
+
+ // Create a non-active AddonInstall so can check that it is returned in the results
+ AddonManager.getInstallForURL(BASE_URL + INSTALL_URL3,
+ run_test_1, "application/x-xpinstall");
+ }, "application/x-xpinstall");
+ });
+}
+
+function end_test() {
+ let testDir = gProfD.clone();
+ testDir.append("extensions");
+ testDir.append("staged");
+ gServer.stop(function() {
+ function loop() {
+ if (!testDir.exists()) {
+ do_print("Staged directory has been cleaned up");
+ do_test_finished();
+ }
+ do_print("Waiting 1 second until cleanup is complete");
+ do_timeout(1000, loop);
+ }
+ loop();
+ });
+}
+
+// Tests homepageURL, getRecommendedURL() and getSearchURL()
+function run_test_1() {
+ function check_urls(aPreference, aGetURL, aTests) {
+ aTests.forEach(function(aTest) {
+ Services.prefs.setCharPref(aPreference, aTest.preferenceValue);
+ do_check_eq(aGetURL(aTest), aTest.expectedURL);
+ });
+ }
+
+ var urlTests = [{
+ preferenceValue: BASE_URL,
+ expectedURL: BASE_URL
+ }, {
+ preferenceValue: BASE_URL + "/%OS%/%VERSION%",
+ expectedURL: BASE_URL + "/XPCShell/1"
+ }];
+
+ // Extra tests for AddonRepository.getSearchURL();
+ var searchURLTests = [{
+ searchTerms: "test",
+ preferenceValue: BASE_URL + "/search?q=%TERMS%",
+ expectedURL: BASE_URL + "/search?q=test"
+ }, {
+ searchTerms: "test search",
+ preferenceValue: BASE_URL + "/%TERMS%",
+ expectedURL: BASE_URL + "/test%20search"
+ }, {
+ searchTerms: "odd=search:with&weird\"characters",
+ preferenceValue: BASE_URL + "/%TERMS%",
+ expectedURL: BASE_URL + "/odd%3Dsearch%3Awith%26weird%22characters"
+ }];
+
+ // Setup tests for homepageURL, getRecommendedURL() and getSearchURL()
+ var tests = [{
+ initiallyUndefined: true,
+ preference: PREF_GETADDONS_BROWSEADDONS,
+ urlTests: urlTests,
+ getURL: () => AddonRepository.homepageURL
+ }, {
+ initiallyUndefined: true,
+ preference: PREF_GETADDONS_BROWSERECOMMENDED,
+ urlTests: urlTests,
+ getURL: () => AddonRepository.getRecommendedURL()
+ }, {
+ initiallyUndefined: false,
+ preference: PREF_GETADDONS_BROWSESEARCHRESULTS,
+ urlTests: urlTests.concat(searchURLTests),
+ getURL: function getSearchURL(aTest) {
+ var searchTerms = aTest && aTest.searchTerms ? aTest.searchTerms
+ : "unused terms";
+ return AddonRepository.getSearchURL(searchTerms);
+ }
+ }];
+
+ tests.forEach(function url_test(aTest) {
+ if (aTest.initiallyUndefined) {
+ // Preference is not defined by default
+ do_check_eq(Services.prefs.getPrefType(aTest.preference),
+ Services.prefs.PREF_INVALID);
+ do_check_eq(aTest.getURL(), DEFAULT_URL);
+ }
+
+ check_urls(aTest.preference, aTest.getURL, aTest.urlTests);
+ });
+
+ run_test_getAddonsByID_fails();
+}
+
+// Tests failure of AddonRepository.getAddonsByIDs()
+function run_test_getAddonsByID_fails() {
+ Services.prefs.setCharPref(GET_TEST.preference, GET_TEST.preferenceValue);
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_throw("searchAddons should not have succeeded");
+ end_test();
+ },
+
+ searchFailed: function() {
+ do_check_false(AddonRepository.isSearching);
+ run_test_getAddonsByID_succeeds();
+ }
+ };
+
+ complete_search(function complete_search_fail_callback(aCallback) {
+ AddonRepository.getAddonsByIDs(GET_TEST.failedIDs, aCallback);
+ }, callback);
+}
+
+// Tests success of AddonRepository.getAddonsByIDs()
+function run_test_getAddonsByID_succeeds() {
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_check_eq(aTotalResults, -1);
+ check_results(aAddonsList, GET_RESULTS, aAddonCount, true);
+ run_test_retrieveRecommended_fails();
+ },
+
+ searchFailed: function() {
+ do_throw("searchAddons should not have failed");
+ end_test();
+ }
+ };
+
+ complete_search(function complete_search_succeed_callback(aCallback) {
+ AddonRepository.getAddonsByIDs(GET_TEST.successfulIDs, aCallback);
+ }, callback);
+}
+
+// Tests failure of AddonRepository.retrieveRecommendedAddons()
+function run_test_retrieveRecommended_fails() {
+ Services.prefs.setCharPref(RECOMMENDED_TEST.preference,
+ RECOMMENDED_TEST.preferenceValue);
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_throw("retrieveRecommendedAddons should not have succeeded");
+ end_test();
+ },
+
+ searchFailed: function() {
+ do_check_false(AddonRepository.isSearching);
+ run_test_retrieveRecommended_succeed();
+ }
+ };
+
+ complete_search(function retrieveRecommended_failing_callback(aCallback) {
+ AddonRepository.retrieveRecommendedAddons(FAILED_MAX_RESULTS, aCallback);
+ }, callback);
+}
+
+// Tests success of AddonRepository.retrieveRecommendedAddons()
+function run_test_retrieveRecommended_succeed() {
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_check_eq(aTotalResults, -1);
+ check_results(aAddonsList, SEARCH_RESULTS, aAddonCount);
+ run_test_searchAddons_fails();
+ },
+
+ searchFailed: function() {
+ do_throw("retrieveRecommendedAddons should not have failed");
+ end_test();
+ }
+ };
+
+ complete_search(function retrieveRecommended_succeed_callback(aCallback) {
+ AddonRepository.retrieveRecommendedAddons(MAX_RESULTS, aCallback);
+ }, callback);
+}
+
+// Tests failure of AddonRepository.searchAddons()
+function run_test_searchAddons_fails() {
+ Services.prefs.setCharPref(SEARCH_TEST.preference, SEARCH_TEST.preferenceValue);
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_throw("searchAddons should not have succeeded");
+ end_test();
+ },
+
+ searchFailed: function() {
+ do_check_false(AddonRepository.isSearching);
+ run_test_searchAddons_succeeds();
+ }
+ };
+
+ complete_search(function(aCallback) {
+ var searchTerms = SEARCH_TEST.searchTerms;
+ AddonRepository.searchAddons(searchTerms, FAILED_MAX_RESULTS, aCallback);
+ }, callback);
+}
+
+// Tests success of AddonRepository.searchAddons()
+function run_test_searchAddons_succeeds() {
+ var callback = {
+ searchSucceeded: function(aAddonsList, aAddonCount, aTotalResults) {
+ do_check_eq(aTotalResults, TOTAL_RESULTS);
+ check_results(aAddonsList, SEARCH_RESULTS, aAddonCount);
+ end_test();
+ },
+
+ searchFailed: function() {
+ do_throw("searchAddons should not have failed");
+ end_test();
+ }
+ };
+
+ complete_search(function(aCallback) {
+ var searchTerms = SEARCH_TEST.searchTerms;
+ AddonRepository.searchAddons(searchTerms, MAX_RESULTS, aCallback);
+ }, callback);
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js
new file mode 100644
index 000000000..203235940
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_cache.js
@@ -0,0 +1,704 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests caching in AddonRepository.jsm
+
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+
+var gServer;
+
+const PORT = 4444;
+const BASE_URL = "http://localhost:" + PORT;
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+const PREF_GETADDONS_CACHE_TYPES = "extensions.getAddons.cache.types";
+const GETADDONS_RESULTS = BASE_URL + "/data/test_AddonRepository_cache.xml";
+const GETADDONS_EMPTY = BASE_URL + "/data/test_AddonRepository_empty.xml";
+const GETADDONS_FAILED = BASE_URL + "/data/test_AddonRepository_failed.xml";
+
+const FILE_DATABASE = "addons.json";
+const ADDON_NAMES = ["test_AddonRepository_1",
+ "test_AddonRepository_2",
+ "test_AddonRepository_3"];
+const ADDON_IDS = ADDON_NAMES.map(aName => aName + "@tests.mozilla.org");
+const ADDON_FILES = ADDON_NAMES.map(do_get_addon);
+
+const PREF_ADDON0_CACHE_ENABLED = "extensions." + ADDON_IDS[0] + ".getAddons.cache.enabled";
+const PREF_ADDON1_CACHE_ENABLED = "extensions." + ADDON_IDS[1] + ".getAddons.cache.enabled";
+
+// Properties of an individual add-on that should be checked
+// Note: size and updateDate are checked separately
+const ADDON_PROPERTIES = ["id", "type", "name", "version", "creator",
+ "developers", "translators", "contributors",
+ "description", "fullDescription",
+ "developerComments", "eula", "iconURL", "icons",
+ "screenshots", "homepageURL", "supportURL",
+ "optionsURL", "aboutURL", "contributionURL",
+ "contributionAmount", "averageRating", "reviewCount",
+ "reviewURL", "totalDownloads", "weeklyDownloads",
+ "dailyUsers", "sourceURI", "repositoryStatus",
+ "compatibilityOverrides"];
+
+// The size and updateDate properties are annoying to test for XPI add-ons.
+// However, since we only care about whether the repository value vs. the
+// XPI value is used, we can just test if the property value matches
+// the repository value
+const REPOSITORY_SIZE = 9;
+const REPOSITORY_UPDATEDATE = 9;
+
+// Get the URI of a subfile locating directly in the folder of
+// the add-on corresponding to the specified id
+function get_subfile_uri(aId, aFilename) {
+ let file = gProfD.clone();
+ file.append("extensions");
+ return do_get_addon_root_uri(file, aId) + aFilename;
+}
+
+
+// Expected repository add-ons
+const REPOSITORY_ADDONS = [{
+ id: ADDON_IDS[0],
+ type: "extension",
+ name: "Repo Add-on 1",
+ version: "2.1",
+ creator: {
+ name: "Repo Add-on 1 - Creator",
+ url: BASE_URL + "/repo/1/creator.html"
+ },
+ developers: [{
+ name: "Repo Add-on 1 - First Developer",
+ url: BASE_URL + "/repo/1/firstDeveloper.html"
+ }, {
+ name: "Repo Add-on 1 - Second Developer",
+ url: BASE_URL + "/repo/1/secondDeveloper.html"
+ }],
+ description: "Repo Add-on 1 - Description\nSecond line",
+ fullDescription: "Repo Add-on 1 - Full Description & some extra",
+ developerComments: "Repo Add-on 1\nDeveloper Comments",
+ eula: "Repo Add-on 1 - EULA",
+ iconURL: BASE_URL + "/repo/1/icon.png",
+ icons: { "32": BASE_URL + "/repo/1/icon.png" },
+ homepageURL: BASE_URL + "/repo/1/homepage.html",
+ supportURL: BASE_URL + "/repo/1/support.html",
+ contributionURL: BASE_URL + "/repo/1/meetDevelopers.html",
+ contributionAmount: "$11.11",
+ averageRating: 1,
+ reviewCount: 1111,
+ reviewURL: BASE_URL + "/repo/1/review.html",
+ totalDownloads: 2221,
+ weeklyDownloads: 3331,
+ dailyUsers: 4441,
+ sourceURI: BASE_URL + "/repo/1/install.xpi",
+ repositoryStatus: 4,
+ compatibilityOverrides: [{
+ type: "incompatible",
+ minVersion: 0.1,
+ maxVersion: 0.2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 3.0,
+ appMaxVersion: 4.0
+ }, {
+ type: "incompatible",
+ minVersion: 0.2,
+ maxVersion: 0.3,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 5.0,
+ appMaxVersion: 6.0
+ }]
+}, {
+ id: ADDON_IDS[1],
+ type: "theme",
+ name: "Repo Add-on 2",
+ version: "2.2",
+ creator: {
+ name: "Repo Add-on 2 - Creator",
+ url: BASE_URL + "/repo/2/creator.html"
+ },
+ developers: [{
+ name: "Repo Add-on 2 - First Developer",
+ url: BASE_URL + "/repo/2/firstDeveloper.html"
+ }, {
+ name: "Repo Add-on 2 - Second Developer",
+ url: BASE_URL + "/repo/2/secondDeveloper.html"
+ }],
+ description: "Repo Add-on 2 - Description",
+ fullDescription: "Repo Add-on 2 - Full Description",
+ developerComments: "Repo Add-on 2 - Developer Comments",
+ eula: "Repo Add-on 2 - EULA",
+ iconURL: BASE_URL + "/repo/2/icon.png",
+ icons: { "32": BASE_URL + "/repo/2/icon.png" },
+ screenshots: [{
+ url: BASE_URL + "/repo/2/firstFull.png",
+ thumbnailURL: BASE_URL + "/repo/2/firstThumbnail.png",
+ caption: "Repo Add-on 2 - First Caption"
+ }, {
+ url: BASE_URL + "/repo/2/secondFull.png",
+ thumbnailURL: BASE_URL + "/repo/2/secondThumbnail.png",
+ caption: "Repo Add-on 2 - Second Caption"
+ }],
+ homepageURL: BASE_URL + "/repo/2/homepage.html",
+ supportURL: BASE_URL + "/repo/2/support.html",
+ contributionURL: BASE_URL + "/repo/2/meetDevelopers.html",
+ contributionAmount: null,
+ averageRating: 2,
+ reviewCount: 1112,
+ reviewURL: BASE_URL + "/repo/2/review.html",
+ totalDownloads: 2222,
+ weeklyDownloads: 3332,
+ dailyUsers: 4442,
+ sourceURI: BASE_URL + "/repo/2/install.xpi",
+ repositoryStatus: 9
+}, {
+ id: ADDON_IDS[2],
+ type: "theme",
+ name: "Repo Add-on 3",
+ version: "2.3",
+ iconURL: BASE_URL + "/repo/3/icon.png",
+ icons: { "32": BASE_URL + "/repo/3/icon.png" },
+ screenshots: [{
+ url: BASE_URL + "/repo/3/firstFull.png",
+ thumbnailURL: BASE_URL + "/repo/3/firstThumbnail.png",
+ caption: "Repo Add-on 3 - First Caption"
+ }, {
+ url: BASE_URL + "/repo/3/secondFull.png",
+ thumbnailURL: BASE_URL + "/repo/3/secondThumbnail.png",
+ caption: "Repo Add-on 3 - Second Caption"
+ }]
+}];
+
+
+// Expected add-ons when not using cache
+const WITHOUT_CACHE = [{
+ id: ADDON_IDS[0],
+ type: "extension",
+ name: "XPI Add-on 1",
+ version: "1.1",
+ creator: { name: "XPI Add-on 1 - Creator" },
+ developers: [{ name: "XPI Add-on 1 - First Developer" },
+ { name: "XPI Add-on 1 - Second Developer" }],
+ translators: [{ name: "XPI Add-on 1 - First Translator" },
+ { name: "XPI Add-on 1 - Second Translator" }],
+ contributors: [{ name: "XPI Add-on 1 - First Contributor" },
+ { name: "XPI Add-on 1 - Second Contributor" }],
+ description: "XPI Add-on 1 - Description",
+ iconURL: BASE_URL + "/xpi/1/icon.png",
+ icons: { "32": BASE_URL + "/xpi/1/icon.png" },
+ homepageURL: BASE_URL + "/xpi/1/homepage.html",
+ optionsURL: BASE_URL + "/xpi/1/options.html",
+ aboutURL: BASE_URL + "/xpi/1/about.html",
+ sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec
+}, {
+ id: ADDON_IDS[1],
+ type: "theme",
+ name: "XPI Add-on 2",
+ version: "1.2",
+ sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec,
+ icons: {}
+}, {
+ id: ADDON_IDS[2],
+ type: "theme",
+ name: "XPI Add-on 3",
+ version: "1.3",
+ get iconURL () {
+ return get_subfile_uri(ADDON_IDS[2], "icon.png");
+ },
+ get icons () {
+ return { "32": get_subfile_uri(ADDON_IDS[2], "icon.png") };
+ },
+ screenshots: [{ get url () { return get_subfile_uri(ADDON_IDS[2], "preview.png"); } }],
+ sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec
+}];
+
+
+// Expected add-ons when using cache
+const WITH_CACHE = [{
+ id: ADDON_IDS[0],
+ type: "extension",
+ name: "XPI Add-on 1",
+ version: "1.1",
+ creator: {
+ name: "Repo Add-on 1 - Creator",
+ url: BASE_URL + "/repo/1/creator.html"
+ },
+ developers: [{ name: "XPI Add-on 1 - First Developer" },
+ { name: "XPI Add-on 1 - Second Developer" }],
+ translators: [{ name: "XPI Add-on 1 - First Translator" },
+ { name: "XPI Add-on 1 - Second Translator" }],
+ contributors: [{ name: "XPI Add-on 1 - First Contributor" },
+ { name: "XPI Add-on 1 - Second Contributor" }],
+ description: "XPI Add-on 1 - Description",
+ fullDescription: "Repo Add-on 1 - Full Description & some extra",
+ developerComments: "Repo Add-on 1\nDeveloper Comments",
+ eula: "Repo Add-on 1 - EULA",
+ iconURL: BASE_URL + "/xpi/1/icon.png",
+ icons: { "32": BASE_URL + "/xpi/1/icon.png" },
+ homepageURL: BASE_URL + "/xpi/1/homepage.html",
+ supportURL: BASE_URL + "/repo/1/support.html",
+ optionsURL: BASE_URL + "/xpi/1/options.html",
+ aboutURL: BASE_URL + "/xpi/1/about.html",
+ contributionURL: BASE_URL + "/repo/1/meetDevelopers.html",
+ contributionAmount: "$11.11",
+ averageRating: 1,
+ reviewCount: 1111,
+ reviewURL: BASE_URL + "/repo/1/review.html",
+ totalDownloads: 2221,
+ weeklyDownloads: 3331,
+ dailyUsers: 4441,
+ sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec,
+ repositoryStatus: 4,
+ compatibilityOverrides: [{
+ type: "incompatible",
+ minVersion: 0.1,
+ maxVersion: 0.2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 3.0,
+ appMaxVersion: 4.0
+ }, {
+ type: "incompatible",
+ minVersion: 0.2,
+ maxVersion: 0.3,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 5.0,
+ appMaxVersion: 6.0
+ }]
+}, {
+ id: ADDON_IDS[1],
+ type: "theme",
+ name: "XPI Add-on 2",
+ version: "1.2",
+ creator: {
+ name: "Repo Add-on 2 - Creator",
+ url: BASE_URL + "/repo/2/creator.html"
+ },
+ developers: [{
+ name: "Repo Add-on 2 - First Developer",
+ url: BASE_URL + "/repo/2/firstDeveloper.html"
+ }, {
+ name: "Repo Add-on 2 - Second Developer",
+ url: BASE_URL + "/repo/2/secondDeveloper.html"
+ }],
+ description: "Repo Add-on 2 - Description",
+ fullDescription: "Repo Add-on 2 - Full Description",
+ developerComments: "Repo Add-on 2 - Developer Comments",
+ eula: "Repo Add-on 2 - EULA",
+ iconURL: BASE_URL + "/repo/2/icon.png",
+ icons: { "32": BASE_URL + "/repo/2/icon.png" },
+ screenshots: [{
+ url: BASE_URL + "/repo/2/firstFull.png",
+ thumbnailURL: BASE_URL + "/repo/2/firstThumbnail.png",
+ caption: "Repo Add-on 2 - First Caption"
+ }, {
+ url: BASE_URL + "/repo/2/secondFull.png",
+ thumbnailURL: BASE_URL + "/repo/2/secondThumbnail.png",
+ caption: "Repo Add-on 2 - Second Caption"
+ }],
+ homepageURL: BASE_URL + "/repo/2/homepage.html",
+ supportURL: BASE_URL + "/repo/2/support.html",
+ contributionURL: BASE_URL + "/repo/2/meetDevelopers.html",
+ contributionAmount: null,
+ averageRating: 2,
+ reviewCount: 1112,
+ reviewURL: BASE_URL + "/repo/2/review.html",
+ totalDownloads: 2222,
+ weeklyDownloads: 3332,
+ dailyUsers: 4442,
+ sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec,
+ repositoryStatus: 9
+}, {
+ id: ADDON_IDS[2],
+ type: "theme",
+ name: "XPI Add-on 3",
+ version: "1.3",
+ get iconURL () {
+ return get_subfile_uri(ADDON_IDS[2], "icon.png");
+ },
+ get icons () {
+ return { "32": get_subfile_uri(ADDON_IDS[2], "icon.png") };
+ },
+ screenshots: [{
+ url: BASE_URL + "/repo/3/firstFull.png",
+ thumbnailURL: BASE_URL + "/repo/3/firstThumbnail.png",
+ caption: "Repo Add-on 3 - First Caption"
+ }, {
+ url: BASE_URL + "/repo/3/secondFull.png",
+ thumbnailURL: BASE_URL + "/repo/3/secondThumbnail.png",
+ caption: "Repo Add-on 3 - Second Caption"
+ }],
+ sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec
+}];
+
+// Expected add-ons when using cache
+const WITH_EXTENSION_CACHE = [{
+ id: ADDON_IDS[0],
+ type: "extension",
+ name: "XPI Add-on 1",
+ version: "1.1",
+ creator: {
+ name: "Repo Add-on 1 - Creator",
+ url: BASE_URL + "/repo/1/creator.html"
+ },
+ developers: [{ name: "XPI Add-on 1 - First Developer" },
+ { name: "XPI Add-on 1 - Second Developer" }],
+ translators: [{ name: "XPI Add-on 1 - First Translator" },
+ { name: "XPI Add-on 1 - Second Translator" }],
+ contributors: [{ name: "XPI Add-on 1 - First Contributor" },
+ { name: "XPI Add-on 1 - Second Contributor" }],
+ description: "XPI Add-on 1 - Description",
+ fullDescription: "Repo Add-on 1 - Full Description & some extra",
+ developerComments: "Repo Add-on 1\nDeveloper Comments",
+ eula: "Repo Add-on 1 - EULA",
+ iconURL: BASE_URL + "/xpi/1/icon.png",
+ icons: { "32": BASE_URL + "/xpi/1/icon.png" },
+ homepageURL: BASE_URL + "/xpi/1/homepage.html",
+ supportURL: BASE_URL + "/repo/1/support.html",
+ optionsURL: BASE_URL + "/xpi/1/options.html",
+ aboutURL: BASE_URL + "/xpi/1/about.html",
+ contributionURL: BASE_URL + "/repo/1/meetDevelopers.html",
+ contributionAmount: "$11.11",
+ averageRating: 1,
+ reviewCount: 1111,
+ reviewURL: BASE_URL + "/repo/1/review.html",
+ totalDownloads: 2221,
+ weeklyDownloads: 3331,
+ dailyUsers: 4441,
+ sourceURI: NetUtil.newURI(ADDON_FILES[0]).spec,
+ repositoryStatus: 4,
+ compatibilityOverrides: [{
+ type: "incompatible",
+ minVersion: 0.1,
+ maxVersion: 0.2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 3.0,
+ appMaxVersion: 4.0
+ }, {
+ type: "incompatible",
+ minVersion: 0.2,
+ maxVersion: 0.3,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 5.0,
+ appMaxVersion: 6.0
+ }]
+}, {
+ id: ADDON_IDS[1],
+ type: "theme",
+ name: "XPI Add-on 2",
+ version: "1.2",
+ sourceURI: NetUtil.newURI(ADDON_FILES[1]).spec,
+ icons: {}
+}, {
+ id: ADDON_IDS[2],
+ type: "theme",
+ name: "XPI Add-on 3",
+ version: "1.3",
+ get iconURL () {
+ return get_subfile_uri(ADDON_IDS[2], "icon.png");
+ },
+ get icons () {
+ return { "32": get_subfile_uri(ADDON_IDS[2], "icon.png") };
+ },
+ screenshots: [{ get url () { return get_subfile_uri(ADDON_IDS[2], "preview.png"); } }],
+ sourceURI: NetUtil.newURI(ADDON_FILES[2]).spec
+}];
+
+var gDBFile = gProfD.clone();
+gDBFile.append(FILE_DATABASE);
+
+/*
+ * Check the actual add-on results against the expected add-on results
+ *
+ * @param aActualAddons
+ * The array of actual add-ons to check
+ * @param aExpectedAddons
+ * The array of expected add-ons to check against
+ * @param aFromRepository
+ * An optional boolean representing if the add-ons are from
+ * the repository
+ */
+function check_results(aActualAddons, aExpectedAddons, aFromRepository) {
+ aFromRepository = !!aFromRepository;
+
+ do_check_addons(aActualAddons, aExpectedAddons, ADDON_PROPERTIES);
+
+ // Separately test size and updateDate (they should only be equal to the
+ // REPOSITORY values if they are from the repository)
+ aActualAddons.forEach(function(aActualAddon) {
+ if (aActualAddon.size)
+ do_check_eq(aActualAddon.size === REPOSITORY_SIZE, aFromRepository);
+
+ if (aActualAddon.updateDate) {
+ let time = aActualAddon.updateDate.getTime();
+ do_check_eq(time === 1000 * REPOSITORY_UPDATEDATE, aFromRepository);
+ }
+ });
+}
+
+/*
+ * Check the add-ons in the cache. This function also tests
+ * AddonRepository.getCachedAddonByID()
+ *
+ * @param aExpectedToFind
+ * An array of booleans representing which REPOSITORY_ADDONS are
+ * expected to be found in the cache
+ * @param aExpectedImmediately
+ * A boolean representing if results from the cache are expected
+ * immediately. Results are not immediate if the cache has not been
+ * initialized yet.
+ * @return Promise{null}
+ * Resolves once the checks are complete
+ */
+function check_cache(aExpectedToFind, aExpectedImmediately) {
+ do_check_eq(aExpectedToFind.length, REPOSITORY_ADDONS.length);
+
+ let lookups = [];
+
+ for (let i = 0 ; i < REPOSITORY_ADDONS.length ; i++) {
+ lookups.push(new Promise((resolve, reject) => {
+ let immediatelyFound = true;
+ let expected = aExpectedToFind[i] ? REPOSITORY_ADDONS[i] : null;
+ // can't Promise-wrap this because we're also testing whether the callback is
+ // sync or async
+ AddonRepository.getCachedAddonByID(REPOSITORY_ADDONS[i].id, function(aAddon) {
+ do_check_eq(immediatelyFound, aExpectedImmediately);
+ if (expected == null)
+ do_check_eq(aAddon, null);
+ else
+ check_results([aAddon], [expected], true);
+ resolve();
+ });
+ immediatelyFound = false;
+ }));
+ }
+ return Promise.all(lookups);
+}
+
+/*
+ * Task to check an initialized cache by checking the cache, then restarting the
+ * manager, and checking the cache. This checks that the cache is consistent
+ * across manager restarts.
+ *
+ * @param aExpectedToFind
+ * An array of booleans representing which REPOSITORY_ADDONS are
+ * expected to be found in the cache
+ */
+function* check_initialized_cache(aExpectedToFind) {
+ yield check_cache(aExpectedToFind, true);
+ yield promiseRestartManager();
+
+ // If cache is disabled, then expect results immediately
+ let cacheEnabled = Services.prefs.getBoolPref(PREF_GETADDONS_CACHE_ENABLED);
+ yield check_cache(aExpectedToFind, !cacheEnabled);
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* setup() {
+ // Setup for test
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ startupManager();
+
+ // Install XPI add-ons
+ yield promiseInstallAllFiles(ADDON_FILES);
+ yield promiseRestartManager();
+
+ gServer = createHttpServer(PORT);
+ gServer.registerDirectory("/data/", do_get_file("data"));
+});
+
+// Tests AddonRepository.cacheEnabled
+add_task(function* run_test_1() {
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ do_check_false(AddonRepository.cacheEnabled);
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ do_check_true(AddonRepository.cacheEnabled);
+});
+
+// Tests that the cache and database begin as empty
+add_task(function* run_test_2() {
+ do_check_false(gDBFile.exists());
+ yield check_cache([false, false, false], false);
+ yield AddonRepository.flush();
+});
+
+// Tests repopulateCache when the search fails
+add_task(function* run_test_3() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
+
+ yield AddonRepository.repopulateCache();
+ yield check_initialized_cache([false, false, false]);
+});
+
+// Tests repopulateCache when search returns no results
+add_task(function* run_test_4() {
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
+
+ yield AddonRepository.repopulateCache();
+ yield check_initialized_cache([false, false, false]);
+});
+
+// Tests repopulateCache when search returns results
+add_task(function* run_test_5() {
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
+
+ yield AddonRepository.repopulateCache();
+ yield check_initialized_cache([true, true, true]);
+});
+
+// Tests repopulateCache when caching is disabled for a single add-on
+add_task(function* run_test_5_1() {
+ Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, false);
+
+ yield AddonRepository.repopulateCache();
+
+ // Reset pref for next test
+ Services.prefs.setBoolPref(PREF_ADDON0_CACHE_ENABLED, true);
+
+ yield check_initialized_cache([false, true, true]);
+});
+
+// Tests repopulateCache when caching is disabled
+add_task(function* run_test_6() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+
+ yield AddonRepository.repopulateCache();
+ // Database should have been deleted
+ do_check_false(gDBFile.exists());
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ yield check_cache([false, false, false], false);
+ yield AddonRepository.flush();
+});
+
+// Tests cacheAddons when the search fails
+add_task(function* run_test_7() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_FAILED);
+
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons(ADDON_IDS, resolve));
+ yield check_initialized_cache([false, false, false]);
+});
+
+// Tests cacheAddons when the search returns no results
+add_task(function* run_test_8() {
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_EMPTY);
+
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons(ADDON_IDS, resolve));
+ yield check_initialized_cache([false, false, false]);
+});
+
+// Tests cacheAddons for a single add-on when search returns results
+add_task(function* run_test_9() {
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
+
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons([ADDON_IDS[0]], resolve));
+ yield check_initialized_cache([true, false, false]);
+});
+
+// Tests cacheAddons when caching is disabled for a single add-on
+add_task(function* run_test_9_1() {
+ Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, false);
+
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons(ADDON_IDS, resolve));
+
+ // Reset pref for next test
+ Services.prefs.setBoolPref(PREF_ADDON1_CACHE_ENABLED, true);
+
+ yield check_initialized_cache([true, false, true]);
+});
+
+// Tests cacheAddons for multiple add-ons, some already in the cache,
+add_task(function* run_test_10() {
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons(ADDON_IDS, resolve));
+ yield check_initialized_cache([true, true, true]);
+});
+
+// Tests cacheAddons when caching is disabled
+add_task(function* run_test_11() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+
+ yield new Promise((resolve, reject) =>
+ AddonRepository.cacheAddons(ADDON_IDS, resolve));
+ do_check_true(gDBFile.exists());
+
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+ yield check_initialized_cache([true, true, true]);
+});
+
+// Tests that XPI add-ons do not use any of the repository properties if
+// caching is disabled, even if there are repository properties available
+add_task(function* run_test_12() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, false);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
+
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITHOUT_CACHE);
+});
+
+// Tests that a background update with caching disabled deletes the add-ons
+// database, and that XPI add-ons still do not use any of repository properties
+add_task(function* run_test_13() {
+ do_check_true(gDBFile.exists());
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_EMPTY);
+
+ yield AddonManagerInternal.backgroundUpdateCheck();
+ // Database should have been deleted
+ do_check_false(gDBFile.exists());
+
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITHOUT_CACHE);
+});
+
+// Tests that the XPI add-ons have the correct properties if caching is
+// enabled but has no information
+add_task(function* run_test_14() {
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ yield AddonManagerInternal.backgroundUpdateCheck();
+ yield AddonRepository.flush();
+ do_check_true(gDBFile.exists());
+
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITHOUT_CACHE);
+});
+
+// Tests that the XPI add-ons correctly use the repository properties when
+// caching is enabled and the repository information is available
+add_task(function* run_test_15() {
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, GETADDONS_RESULTS);
+
+ yield AddonManagerInternal.backgroundUpdateCheck();
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITH_CACHE);
+});
+
+// Tests that restarting the manager does not change the checked properties
+// on the XPI add-ons (repository properties still exist and are still properly
+// used)
+add_task(function* run_test_16() {
+ yield promiseRestartManager();
+
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITH_CACHE);
+});
+
+// Tests that setting a list of types to cache works
+add_task(function* run_test_17() {
+ Services.prefs.setCharPref(PREF_GETADDONS_CACHE_TYPES, "foo,bar,extension,baz");
+
+ yield AddonManagerInternal.backgroundUpdateCheck();
+ let aAddons = yield promiseAddonsByIDs(ADDON_IDS);
+ check_results(aAddons, WITH_EXTENSION_CACHE);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_compatmode.js b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_compatmode.js
new file mode 100644
index 000000000..6aec96ea1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_AddonRepository_compatmode.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that AddonRepository correctly fills in the
+// %COMPATIBILITY_MODE% token in the Search API URL.
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+var COMPATIBILITY_PREF;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_AddonRepository_compatmode_ignore.xml", gServer);
+mapFile("/data/test_AddonRepository_compatmode_normal.xml", gServer);
+mapFile("/data/test_AddonRepository_compatmode_strict.xml", gServer);
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS,
+ "http://localhost:" + gPort + "/data/test_AddonRepository_compatmode_%COMPATIBILITY_MODE%.xml");
+ startupManager();
+ run_test_1();
+}
+
+function end_test() {
+ gServer.stop(do_test_finished);
+}
+
+// Strict compatibility checking disabled.
+function run_test_1() {
+ do_print("Testing with strict compatibility checking disabled");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+ AddonRepository.searchAddons("test", 6, {
+ searchSucceeded: function(aAddons) {
+ do_check_neq(aAddons, null);
+ do_check_eq(aAddons.length, 1);
+ do_check_eq(aAddons[0].id, "compatmode-normal@tests.mozilla.org");
+
+ run_test_2();
+ },
+ searchFailed: function() {
+ do_throw("Search should not have failed");
+ }
+ });
+}
+
+// Strict compatibility checking enabled.
+function run_test_2() {
+ do_print("Testing with strict compatibility checking enabled");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+ AddonRepository.searchAddons("test", 6, {
+ searchSucceeded: function(aAddons) {
+ do_check_neq(aAddons, null);
+ do_check_eq(aAddons.length, 1);
+ do_check_eq(aAddons[0].id, "compatmode-strict@tests.mozilla.org");
+
+ run_test_3();
+ },
+ searchFailed: function() {
+ do_throw("Search should not have failed");
+ }
+ });
+}
+
+// Compatibility checking disabled.
+function run_test_3() {
+ do_print("Testing with all compatibility checking disabled");
+ AddonManager.checkCompatibility = false;
+
+ AddonRepository.searchAddons("test", 6, {
+ searchSucceeded: function(aAddons) {
+ do_check_neq(aAddons, null);
+ do_check_eq(aAddons.length, 1);
+ do_check_eq(aAddons[0].id, "compatmode-ignore@tests.mozilla.org");
+
+ end_test();
+ },
+ searchFailed: function() {
+ do_throw("Search should not have failed");
+ }
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ChromeManifestParser.js b/toolkit/mozapps/extensions/test/xpcshell/test_ChromeManifestParser.js
new file mode 100644
index 000000000..605c4224b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_ChromeManifestParser.js
@@ -0,0 +1,108 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests ChromeManifestParser.js
+
+Components.utils.import("resource://gre/modules/ChromeManifestParser.jsm");
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ startupManager();
+
+ installAllFiles([do_get_addon("test_chromemanifest_1"),
+ do_get_addon("test_chromemanifest_2"),
+ do_get_addon("test_chromemanifest_3"),
+ do_get_addon("test_chromemanifest_4")],
+ function() {
+
+ restartManager();
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+ // addon1
+ let a1Uri = a1.getResourceURI("/").spec;
+ let expected = [
+ {type: "content", baseURI: a1Uri, args: ["test-addon-1", "chrome/content"]},
+ {type: "locale", baseURI: a1Uri, args: ["test-addon-1", "en-US", "locale/en-US"]},
+ {type: "locale", baseURI: a1Uri, args: ["test-addon-1", "fr-FR", "locale/fr-FR"]},
+ {type: "overlay", baseURI: a1Uri, args: ["chrome://browser/content/browser.xul", "chrome://test-addon-1/content/overlay.xul"]}
+ ];
+ let manifestURI = a1.getResourceURI("chrome.manifest");
+ let manifest = ChromeManifestParser.parseSync(manifestURI);
+
+ do_check_true(Array.isArray(manifest));
+ do_check_eq(manifest.length, expected.length);
+ for (let i = 0; i < manifest.length; i++) {
+ do_check_eq(JSON.stringify(manifest[i]), JSON.stringify(expected[i]));
+ }
+
+ // addon2
+ let a2Uri = a2.getResourceURI("/").spec;
+ expected = [
+ {type: "content", baseURI: a2Uri, args: ["test-addon-1", "chrome/content"]},
+ {type: "locale", baseURI: a2Uri, args: ["test-addon-1", "en-US", "locale/en-US"]},
+ {type: "locale", baseURI: a2Uri, args: ["test-addon-1", "fr-FR", "locale/fr-FR"]},
+ {type: "overlay", baseURI: a2Uri, args: ["chrome://browser/content/browser.xul", "chrome://test-addon-1/content/overlay.xul"]},
+ {type: "binary-component", baseURI: a2Uri, args: ["components/something.so"]}
+ ];
+ manifestURI = a2.getResourceURI("chrome.manifest");
+ manifest = ChromeManifestParser.parseSync(manifestURI);
+
+ do_check_true(Array.isArray(manifest));
+ do_check_eq(manifest.length, expected.length);
+ for (let i = 0; i < manifest.length; i++) {
+ do_check_eq(JSON.stringify(manifest[i]), JSON.stringify(expected[i]));
+ }
+
+ // addon3
+ let a3Uri = a3.getResourceURI("/").spec;
+ expected = [
+ {type: "content", baseURI: a3Uri, args: ["test-addon-1", "chrome/content"]},
+ {type: "locale", baseURI: a3Uri, args: ["test-addon-1", "en-US", "locale/en-US"]},
+ {type: "locale", baseURI: a3Uri, args: ["test-addon-1", "fr-FR", "locale/fr-FR"]},
+ {type: "overlay", baseURI: a3Uri, args: ["chrome://browser/content/browser.xul", "chrome://test-addon-1/content/overlay.xul"]},
+ {type: "binary-component", baseURI: a3Uri, args: ["components/something.so"]},
+ {type: "locale", baseURI: "jar:" + a3.getResourceURI("/inner.jar").spec + "!/", args: ["test-addon-1", "en-NZ", "locale/en-NZ"]},
+ ];
+ manifestURI = a3.getResourceURI("chrome.manifest");
+ manifest = ChromeManifestParser.parseSync(manifestURI);
+
+ do_check_true(Array.isArray(manifest));
+ do_check_eq(manifest.length, expected.length);
+ for (let i = 0; i < manifest.length; i++) {
+ do_check_eq(JSON.stringify(manifest[i]), JSON.stringify(expected[i]));
+ }
+
+ // addon4
+ let a4Uri = a4.getResourceURI("/").spec;
+ expected = [
+ {type: "content", baseURI: a4Uri, args: ["test-addon-1", "chrome/content"]},
+ {type: "locale", baseURI: a4Uri, args: ["test-addon-1", "en-US", "locale/en-US"]},
+ {type: "locale", baseURI: a4Uri, args: ["test-addon-1", "fr-FR", "locale/fr-FR"]},
+ {type: "overlay", baseURI: a4Uri, args: ["chrome://browser/content/browser.xul", "chrome://test-addon-1/content/overlay.xul"]},
+ {type: "binary-component", baseURI: a4.getResourceURI("components/").spec, args: ["mycomponent.dll"]},
+ {type: "binary-component", baseURI: a4.getResourceURI("components/other/").spec, args: ["thermalnuclearwar.dll"]}
+ ];
+ manifestURI = a4.getResourceURI("chrome.manifest");
+ manifest = ChromeManifestParser.parseSync(manifestURI);
+
+ do_check_true(Array.isArray(manifest));
+ do_check_eq(manifest.length, expected.length);
+ for (let i = 0; i < manifest.length; i++) {
+ do_check_eq(JSON.stringify(manifest[i]), JSON.stringify(expected[i]));
+ }
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js b/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js
new file mode 100644
index 000000000..2a6ff291e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_DeferredSave.js
@@ -0,0 +1,549 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test behaviour of module to perform deferred save of data
+// files to disk
+
+"use strict";
+
+const testFile = gProfD.clone();
+testFile.append("DeferredSaveTest");
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+var DSContext = Components.utils.import("resource://gre/modules/DeferredSave.jsm", {});
+var DeferredSave = DSContext.DeferredSave;
+
+// Test wrapper to let us do promise/task based testing of DeferredSave
+function DeferredSaveTester(aDataProvider) {
+ let tester = {
+ // Deferred for the promise returned by the mock writeAtomic
+ waDeferred: null,
+
+ // The most recent data "written" by the mock OS.File.writeAtomic
+ writtenData: undefined,
+
+ dataToSave: "Data to save",
+
+ save: (aData, aWriteHandler) => {
+ tester.writeHandler = aWriteHandler || writer;
+ tester.dataToSave = aData;
+ return tester.saver.saveChanges();
+ },
+
+ flush: (aWriteHandler) => {
+ tester.writeHandler = aWriteHandler || writer;
+ return tester.saver.flush();
+ },
+
+ get lastError() {
+ return tester.saver.lastError;
+ }
+ };
+
+ // Default write handler for most cases where the test case doesn't need
+ // to do anything while the write is in progress; just completes the write
+ // on the next event loop
+ function writer(aTester) {
+ do_print("default write callback");
+ let length = aTester.writtenData.length;
+ do_execute_soon(() => aTester.waDeferred.resolve(length));
+ }
+
+ if (!aDataProvider)
+ aDataProvider = () => tester.dataToSave;
+
+ tester.saver = new DeferredSave(testFile.path, aDataProvider);
+
+ // Install a mock for OS.File.writeAtomic to let us control the async
+ // behaviour of the promise
+ DSContext.OS.File.writeAtomic = function mock_writeAtomic(aFile, aData, aOptions) {
+ do_print("writeAtomic: " + aFile + " data: '" + aData + "', " + aOptions.toSource());
+ tester.writtenData = aData;
+ tester.waDeferred = Promise.defer();
+ tester.writeHandler(tester);
+ return tester.waDeferred.promise;
+ };
+
+ return tester;
+}
+
+/**
+ * Install a mock nsITimer factory that triggers on the next spin of
+ * the event loop after it is scheduled
+ */
+function setQuickMockTimer() {
+ let quickTimer = {
+ initWithCallback: function(aFunction, aDelay, aType) {
+ do_print("Starting quick timer, delay = " + aDelay);
+ do_execute_soon(aFunction);
+ },
+ cancel: function() {
+ do_throw("Attempted to cancel a quickMockTimer");
+ }
+ };
+ DSContext.MakeTimer = () => {
+ do_print("Creating quick timer");
+ return quickTimer;
+ };
+}
+
+/**
+ * Install a mock nsITimer factory in DeferredSave.jsm, returning a promise that resolves
+ * when the client code sets the timer. Test cases can use this to wait for client code to
+ * be ready for a timer event, and then signal the event by calling mockTimer.callback().
+ * This could use some enhancement; clients can re-use the returned timer,
+ * but with this implementation it's not possible for the test to wait for
+ * a second call to initWithCallback() on the re-used timer.
+ * @return Promise{mockTimer} that resolves when initWithCallback()
+ * is called
+ */
+function setPromiseMockTimer() {
+ let waiter = Promise.defer();
+ let mockTimer = {
+ callback: null,
+ delay: null,
+ type: null,
+ isCancelled: false,
+
+ initWithCallback: function(aFunction, aDelay, aType) {
+ do_print("Starting timer, delay = " + aDelay);
+ this.callback = aFunction;
+ this.delay = aDelay;
+ this.type = aType;
+ // cancelled timers can be re-used
+ this.isCancelled = false;
+ waiter.resolve(this);
+ },
+ cancel: function() {
+ do_print("Cancelled mock timer");
+ this.callback = null;
+ this.delay = null;
+ this.type = null;
+ this.isCancelled = true;
+ // If initWithCallback was never called, resolve to let tests check for cancel
+ waiter.resolve(this);
+ }
+ };
+ DSContext.MakeTimer = () => {
+ do_print("Creating mock timer");
+ return mockTimer;
+ };
+ return waiter.promise;
+}
+
+/**
+ * Return a Promise<null> that resolves after the specified number of milliseconds
+ */
+function delay(aDelayMS) {
+ let deferred = Promise.defer();
+ do_timeout(aDelayMS, () => deferred.resolve(null));
+ return deferred.promise;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+// Modify set data once, ask for save, make sure it saves cleanly
+add_task(function* test_basic_save_succeeds() {
+ setQuickMockTimer();
+ let tester = DeferredSaveTester();
+ let data = "Test 1 Data";
+
+ yield tester.save(data);
+ do_check_eq(tester.writtenData, data);
+ do_check_eq(1, tester.saver.totalSaves);
+});
+
+// Two saves called during the same event loop, both with callbacks
+// Make sure we save only the second version of the data
+add_task(function* test_two_saves() {
+ setQuickMockTimer();
+ let tester = DeferredSaveTester();
+ let firstCallback_happened = false;
+ let firstData = "Test first save";
+ let secondData = "Test second save";
+
+ // first save should not resolve until after the second one is called,
+ // so we can't just yield this promise
+ tester.save(firstData).then(count => {
+ do_check_eq(secondData, tester.writtenData);
+ do_check_false(firstCallback_happened);
+ firstCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ yield tester.save(secondData);
+ do_check_true(firstCallback_happened);
+ do_check_eq(secondData, tester.writtenData);
+ do_check_eq(1, tester.saver.totalSaves);
+});
+
+// Two saves called with a delay in between, both with callbacks
+// Make sure we save the second version of the data
+add_task(function* test_two_saves_delay() {
+ let timerPromise = setPromiseMockTimer();
+ let tester = DeferredSaveTester();
+ let firstCallback_happened = false;
+ let delayDone = false;
+
+ let firstData = "First data to save with delay";
+ let secondData = "Modified data to save with delay";
+
+ tester.save(firstData).then(count => {
+ do_check_false(firstCallback_happened);
+ do_check_true(delayDone);
+ do_check_eq(secondData, tester.writtenData);
+ firstCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ // Wait a short time to let async events possibly spawned by the
+ // first tester.save() to run
+ yield delay(2);
+ delayDone = true;
+ // request to save modified data
+ let saving = tester.save(secondData);
+ // Yield to wait for client code to set the timer
+ let activeTimer = yield timerPromise;
+ // and then trigger it
+ activeTimer.callback();
+ // now wait for the DeferredSave to finish saving
+ yield saving;
+ do_check_true(firstCallback_happened);
+ do_check_eq(secondData, tester.writtenData);
+ do_check_eq(1, tester.saver.totalSaves);
+ do_check_eq(0, tester.saver.overlappedSaves);
+});
+
+// Test case where OS.File immediately reports an error when the write begins
+// Also check that the "error" getter correctly returns the error
+// Then do a write that succeeds, and make sure the error is cleared
+add_task(function* test_error_immediate() {
+ let tester = DeferredSaveTester();
+ let testError = new Error("Forced failure");
+ function writeFail(aTester) {
+ aTester.waDeferred.reject(testError);
+ }
+
+ setQuickMockTimer();
+ yield tester.save("test_error_immediate", writeFail).then(
+ count => do_throw("Did not get expected error"),
+ error => do_check_eq(testError.message, error.message)
+ );
+ do_check_eq(testError, tester.lastError);
+
+ // This write should succeed and clear the error
+ yield tester.save("test_error_immediate succeeds");
+ do_check_eq(null, tester.lastError);
+ // The failed save attempt counts in our total
+ do_check_eq(2, tester.saver.totalSaves);
+});
+
+// Save one set of changes, then while the write is in progress, modify the
+// data two more times. Test that we re-write the dirty data exactly once
+// after the first write succeeds
+add_task(function* dirty_while_writing() {
+ let tester = DeferredSaveTester();
+ let firstData = "First data";
+ let secondData = "Second data";
+ let thirdData = "Third data";
+ let firstCallback_happened = false;
+ let secondCallback_happened = false;
+ let writeStarted = Promise.defer();
+
+ function writeCallback(aTester) {
+ writeStarted.resolve(aTester.waDeferred);
+ }
+
+ setQuickMockTimer();
+ do_print("First save");
+ tester.save(firstData, writeCallback).then(
+ count => {
+ do_check_false(firstCallback_happened);
+ do_check_false(secondCallback_happened);
+ do_check_eq(tester.writtenData, firstData);
+ firstCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ do_print("waiting for writer");
+ let writer = yield writeStarted.promise;
+ do_print("Write started");
+
+ // Delay a bit, modify the data and call saveChanges, delay a bit more,
+ // modify the data and call saveChanges again, another delay,
+ // then complete the in-progress write
+ yield delay(1);
+
+ tester.save(secondData).then(
+ count => {
+ do_check_true(firstCallback_happened);
+ do_check_false(secondCallback_happened);
+ do_check_eq(tester.writtenData, thirdData);
+ secondCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ // wait and then do the third change
+ yield delay(1);
+ let thirdWrite = tester.save(thirdData);
+
+ // wait a bit more and then finally finish the first write
+ yield delay(1);
+ writer.resolve(firstData.length);
+
+ // Now let everything else finish
+ yield thirdWrite;
+ do_check_true(firstCallback_happened);
+ do_check_true(secondCallback_happened);
+ do_check_eq(tester.writtenData, thirdData);
+ do_check_eq(2, tester.saver.totalSaves);
+ do_check_eq(1, tester.saver.overlappedSaves);
+});
+
+// A write callback for the OS.File.writeAtomic mock that rejects write attempts
+function disabled_write_callback(aTester) {
+ do_throw("Should not have written during clean flush");
+}
+
+// special write callback that disables itself to make sure
+// we don't try to write twice
+function write_then_disable(aTester) {
+ do_print("write_then_disable");
+ let length = aTester.writtenData.length;
+ aTester.writeHandler = disabled_write_callback;
+ do_execute_soon(() => aTester.waDeferred.resolve(length));
+}
+
+// Flush tests. First, do an ordinary clean save and then call flush;
+// there should not be another save
+add_task(function* flush_after_save() {
+ setQuickMockTimer();
+ let tester = DeferredSaveTester();
+ let dataToSave = "Flush after save";
+
+ yield tester.save(dataToSave);
+ yield tester.flush(disabled_write_callback);
+ do_check_eq(1, tester.saver.totalSaves);
+});
+
+// Flush while a write is in progress, but the in-memory data is clean
+add_task(function* flush_during_write() {
+ let tester = DeferredSaveTester();
+ let dataToSave = "Flush during write";
+ let firstCallback_happened = false;
+ let writeStarted = Promise.defer();
+
+ function writeCallback(aTester) {
+ writeStarted.resolve(aTester.waDeferred);
+ }
+
+ setQuickMockTimer();
+ tester.save(dataToSave, writeCallback).then(
+ count => {
+ do_check_false(firstCallback_happened);
+ firstCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ let writer = yield writeStarted.promise;
+
+ // call flush with the write callback disabled, delay a bit more, complete in-progress write
+ let flushing = tester.flush(disabled_write_callback);
+ yield delay(2);
+ writer.resolve(dataToSave.length);
+
+ // now wait for the flush to finish
+ yield flushing;
+ do_check_true(firstCallback_happened);
+ do_check_eq(1, tester.saver.totalSaves);
+});
+
+// Flush while dirty but write not in progress
+// The data written should be the value at the time
+// flush() is called, even if it is changed later
+add_task(function* flush_while_dirty() {
+ let timerPromise = setPromiseMockTimer();
+ let tester = DeferredSaveTester();
+ let firstData = "Flush while dirty, valid data";
+ let firstCallback_happened = false;
+
+ tester.save(firstData, write_then_disable).then(
+ count => {
+ do_check_false(firstCallback_happened);
+ firstCallback_happened = true;
+ do_check_eq(tester.writtenData, firstData);
+ }, do_report_unexpected_exception);
+
+ // Wait for the timer to be set, but don't trigger it so the write won't start
+ let activeTimer = yield timerPromise;
+
+ let flushing = tester.flush();
+
+ // Make sure the timer was cancelled
+ do_check_true(activeTimer.isCancelled);
+
+ // Also make sure that data changed after the flush call
+ // (even without a saveChanges() call) doesn't get written
+ tester.dataToSave = "Flush while dirty, invalid data";
+
+ yield flushing;
+ do_check_true(firstCallback_happened);
+ do_check_eq(tester.writtenData, firstData);
+ do_check_eq(1, tester.saver.totalSaves);
+});
+
+// And the grand finale - modify the data, start writing,
+// modify the data again so we're in progress and dirty,
+// then flush, then modify the data again
+// Data for the second write should be taken at the time
+// flush() is called, even if it is modified later
+add_task(function* flush_writing_dirty() {
+ let timerPromise = setPromiseMockTimer();
+ let tester = DeferredSaveTester();
+ let firstData = "Flush first pass data";
+ let secondData = "Flush second pass data";
+ let firstCallback_happened = false;
+ let secondCallback_happened = false;
+ let writeStarted = Promise.defer();
+
+ function writeCallback(aTester) {
+ writeStarted.resolve(aTester.waDeferred);
+ }
+
+ tester.save(firstData, writeCallback).then(
+ count => {
+ do_check_false(firstCallback_happened);
+ do_check_eq(tester.writtenData, firstData);
+ firstCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ // Trigger the timer callback as soon as the DeferredSave sets it
+ let activeTimer = yield timerPromise;
+ activeTimer.callback();
+ let writer = yield writeStarted.promise;
+ // the first write has started
+
+ // dirty the data and request another save
+ // after the second save completes, there should not be another write
+ tester.save(secondData, write_then_disable).then(
+ count => {
+ do_check_true(firstCallback_happened);
+ do_check_false(secondCallback_happened);
+ do_check_eq(tester.writtenData, secondData);
+ secondCallback_happened = true;
+ }, do_report_unexpected_exception);
+
+ let flushing = tester.flush(write_then_disable);
+ // Flush should have cancelled our timer
+ do_check_true(activeTimer.isCancelled);
+ tester.dataToSave = "Flush, invalid data: changed late";
+ // complete the first write
+ writer.resolve(firstData.length);
+ // now wait for the second write / flush to complete
+ yield flushing;
+ do_check_true(firstCallback_happened);
+ do_check_true(secondCallback_happened);
+ do_check_eq(tester.writtenData, secondData);
+ do_check_eq(2, tester.saver.totalSaves);
+ do_check_eq(1, tester.saver.overlappedSaves);
+});
+
+// A data provider callback that throws an error the first
+// time it is called, and a different error the second time
+// so that tests can (a) make sure the promise is rejected
+// with the error and (b) make sure the provider is only
+// called once in case of error
+const expectedDataError = "Failed to serialize data";
+var badDataError = null;
+function badDataProvider() {
+ let err = new Error(badDataError);
+ badDataError = "badDataProvider called twice";
+ throw err;
+}
+
+// Handle cases where data provider throws
+// First, throws during a normal save
+add_task(function* data_throw() {
+ setQuickMockTimer();
+ badDataError = expectedDataError;
+ let tester = DeferredSaveTester(badDataProvider);
+ yield tester.save("data_throw").then(
+ count => do_throw("Expected serialization failure"),
+ error => do_check_eq(error.message, expectedDataError));
+});
+
+// Now, throws during flush
+add_task(function* data_throw_during_flush() {
+ badDataError = expectedDataError;
+ let tester = DeferredSaveTester(badDataProvider);
+ let firstCallback_happened = false;
+
+ setPromiseMockTimer();
+ // Write callback should never be called
+ tester.save("data_throw_during_flush", disabled_write_callback).then(
+ count => do_throw("Expected serialization failure"),
+ error => {
+ do_check_false(firstCallback_happened);
+ do_check_eq(error.message, expectedDataError);
+ firstCallback_happened = true;
+ });
+
+ // flush() will cancel the timer
+ yield tester.flush(disabled_write_callback).then(
+ count => do_throw("Expected serialization failure"),
+ error => do_check_eq(error.message, expectedDataError)
+ );
+
+ do_check_true(firstCallback_happened);
+});
+
+// Try to reproduce race condition. The observed sequence of events:
+// saveChanges
+// start writing
+// saveChanges
+// finish writing (need to restart delayed timer)
+// saveChanges
+// flush
+// write starts
+// actually restart timer for delayed write
+// write completes
+// delayed timer goes off, throws error because DeferredSave has been torn down
+add_task(function* delay_flush_race() {
+ let timerPromise = setPromiseMockTimer();
+ let tester = DeferredSaveTester();
+ let firstData = "First save";
+ let secondData = "Second save";
+ let thirdData = "Third save";
+ let writeStarted = Promise.defer();
+
+ function writeCallback(aTester) {
+ writeStarted.resolve(aTester.waDeferred);
+ }
+
+ // This promise won't resolve until after writeStarted
+ let firstSave = tester.save(firstData, writeCallback);
+ (yield timerPromise).callback();
+
+ let writer = yield writeStarted.promise;
+ // the first write has started
+
+ // dirty the data and request another save
+ let secondSave = tester.save(secondData);
+
+ // complete the first write
+ writer.resolve(firstData.length);
+ yield firstSave;
+ do_check_eq(tester.writtenData, firstData);
+
+ tester.save(thirdData);
+ let flushing = tester.flush();
+
+ yield secondSave;
+ do_check_eq(tester.writtenData, thirdData);
+
+ yield flushing;
+ do_check_eq(tester.writtenData, thirdData);
+
+ // Our DeferredSave should not have a _timer here; if it
+ // does, the bug caused a reschedule
+ do_check_eq(null, tester.saver._timer);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js b/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js
new file mode 100644
index 000000000..61a46b251
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_LightweightThemeManager.js
@@ -0,0 +1,598 @@
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const MANDATORY = ["id", "name", "headerURL"];
+const OPTIONAL = ["footerURL", "textcolor", "accentcolor", "iconURL",
+ "previewURL", "author", "description", "homepageURL",
+ "updateURL", "version"];
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+function dummy(id) {
+ return {
+ id: id || Math.random().toString(),
+ name: Math.random().toString(),
+ headerURL: "http://lwttest.invalid/a.png",
+ footerURL: "http://lwttest.invalid/b.png",
+ textcolor: Math.random().toString(),
+ accentcolor: Math.random().toString()
+ };
+}
+
+function hasPermission(aAddon, aPerm) {
+ var perm = AddonManager["PERM_CAN_" + aPerm.toUpperCase()];
+ return !!(aAddon.permissions & perm);
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+ startupManager();
+
+ Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 8);
+
+ let {LightweightThemeManager: ltm} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {});
+
+ do_check_eq(typeof ltm, "object");
+ do_check_eq(typeof ltm.usedThemes, "object");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ ltm.previewTheme(dummy("preview0"));
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ ltm.previewTheme(dummy("preview1"));
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+ ltm.resetPreview();
+
+ ltm.currentTheme = dummy("x0");
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.id, "x0");
+ do_check_eq(ltm.usedThemes[0].id, "x0");
+ do_check_eq(ltm.getUsedTheme("x0").id, "x0");
+
+ ltm.previewTheme(dummy("preview0"));
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.id, "x0");
+
+ ltm.resetPreview();
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.id, "x0");
+
+ ltm.currentTheme = dummy("x1");
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.id, "x1");
+ do_check_eq(ltm.usedThemes[1].id, "x0");
+
+ ltm.currentTheme = dummy("x2");
+ do_check_eq(ltm.usedThemes.length, 3);
+ do_check_eq(ltm.currentTheme.id, "x2");
+ do_check_eq(ltm.usedThemes[1].id, "x1");
+ do_check_eq(ltm.usedThemes[2].id, "x0");
+
+ ltm.currentTheme = dummy("x3");
+ ltm.currentTheme = dummy("x4");
+ ltm.currentTheme = dummy("x5");
+ ltm.currentTheme = dummy("x6");
+ ltm.currentTheme = dummy("x7");
+ do_check_eq(ltm.usedThemes.length, 8);
+ do_check_eq(ltm.currentTheme.id, "x7");
+ do_check_eq(ltm.usedThemes[1].id, "x6");
+ do_check_eq(ltm.usedThemes[7].id, "x0");
+
+ ltm.currentTheme = dummy("x8");
+ do_check_eq(ltm.usedThemes.length, 8);
+ do_check_eq(ltm.currentTheme.id, "x8");
+ do_check_eq(ltm.usedThemes[1].id, "x7");
+ do_check_eq(ltm.usedThemes[7].id, "x1");
+ do_check_eq(ltm.getUsedTheme("x0"), null);
+
+ ltm.forgetUsedTheme("nonexistent");
+ do_check_eq(ltm.usedThemes.length, 8);
+ do_check_neq(ltm.currentTheme, null);
+
+ ltm.forgetUsedTheme("x8");
+ do_check_eq(ltm.usedThemes.length, 7);
+ do_check_eq(ltm.currentTheme, null);
+ do_check_eq(ltm.usedThemes[0].id, "x7");
+ do_check_eq(ltm.usedThemes[6].id, "x1");
+
+ ltm.forgetUsedTheme("x7");
+ ltm.forgetUsedTheme("x6");
+ ltm.forgetUsedTheme("x5");
+ ltm.forgetUsedTheme("x4");
+ ltm.forgetUsedTheme("x3");
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme, null);
+ do_check_eq(ltm.usedThemes[0].id, "x2");
+ do_check_eq(ltm.usedThemes[1].id, "x1");
+
+ ltm.currentTheme = dummy("x1");
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.id, "x1");
+ do_check_eq(ltm.usedThemes[0].id, "x1");
+ do_check_eq(ltm.usedThemes[1].id, "x2");
+
+ ltm.currentTheme = dummy("x2");
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.id, "x2");
+ do_check_eq(ltm.usedThemes[0].id, "x2");
+ do_check_eq(ltm.usedThemes[1].id, "x1");
+
+ ltm.currentTheme = ltm.getUsedTheme("x1");
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.id, "x1");
+ do_check_eq(ltm.usedThemes[0].id, "x1");
+ do_check_eq(ltm.usedThemes[1].id, "x2");
+
+ ltm.forgetUsedTheme("x1");
+ ltm.forgetUsedTheme("x2");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ // Use chinese name to test utf-8, for bug #541943
+ var chineseTheme = dummy("chinese0");
+ chineseTheme.name = "笢æ…0";
+ chineseTheme.description = "笢æ…1";
+ ltm.currentTheme = chineseTheme;
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.name, "笢æ…0");
+ do_check_eq(ltm.currentTheme.description, "笢æ…1");
+ do_check_eq(ltm.usedThemes[0].name, "笢æ…0");
+ do_check_eq(ltm.usedThemes[0].description, "笢æ…1");
+ do_check_eq(ltm.getUsedTheme("chinese0").name, "笢æ…0");
+ do_check_eq(ltm.getUsedTheme("chinese0").description, "笢æ…1");
+
+ // This name used to break the usedTheme JSON causing all LWTs to be lost
+ var chineseTheme1 = dummy("chinese1");
+ chineseTheme1.name = "眵昜湮桵蔗åŒ~郔乾";
+ chineseTheme1.description = "眵昜湮桵蔗åŒ~郔乾";
+ ltm.currentTheme = chineseTheme1;
+ do_check_neq(ltm.currentTheme, null);
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.name, "眵昜湮桵蔗åŒ~郔乾");
+ do_check_eq(ltm.currentTheme.description, "眵昜湮桵蔗åŒ~郔乾");
+ do_check_eq(ltm.usedThemes[1].name, "笢æ…0");
+ do_check_eq(ltm.usedThemes[1].description, "笢æ…1");
+ do_check_eq(ltm.usedThemes[0].name, "眵昜湮桵蔗åŒ~郔乾");
+ do_check_eq(ltm.usedThemes[0].description, "眵昜湮桵蔗åŒ~郔乾");
+
+ ltm.forgetUsedTheme("chinese0");
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_neq(ltm.currentTheme, null);
+
+ ltm.forgetUsedTheme("chinese1");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ do_check_eq(ltm.parseTheme("invalid json"), null);
+ do_check_eq(ltm.parseTheme('"json string"'), null);
+
+ function roundtrip(data, secure) {
+ return ltm.parseTheme(JSON.stringify(data),
+ "http" + (secure ? "s" : "") + "://lwttest.invalid/");
+ }
+
+ var data = dummy();
+ do_check_neq(roundtrip(data), null);
+ data.id = null;
+ do_check_eq(roundtrip(data), null);
+ data.id = 1;
+ do_check_eq(roundtrip(data), null);
+ data.id = 1.5;
+ do_check_eq(roundtrip(data), null);
+ data.id = true;
+ do_check_eq(roundtrip(data), null);
+ data.id = {};
+ do_check_eq(roundtrip(data), null);
+ data.id = [];
+ do_check_eq(roundtrip(data), null);
+
+ // Check whether parseTheme handles international characters right
+ var chineseTheme2 = dummy();
+ chineseTheme2.name = "眵昜湮桵蔗åŒ~郔乾";
+ chineseTheme2.description = "眵昜湮桵蔗åŒ~郔乾";
+ do_check_neq(roundtrip(chineseTheme2), null);
+ do_check_eq(roundtrip(chineseTheme2).name, "眵昜湮桵蔗åŒ~郔乾");
+ do_check_eq(roundtrip(chineseTheme2).description, "眵昜湮桵蔗åŒ~郔乾");
+
+ data = dummy();
+ data.unknownProperty = "Foo";
+ do_check_eq(typeof roundtrip(data).unknownProperty, "undefined");
+
+ data = dummy();
+ data.unknownURL = "http://lwttest.invalid/";
+ do_check_eq(typeof roundtrip(data).unknownURL, "undefined");
+
+ function roundtripSet(props, modify, test, secure) {
+ props.forEach(function (prop) {
+ var theme = dummy();
+ modify(theme, prop);
+ test(roundtrip(theme, secure), prop, theme);
+ });
+ }
+
+ roundtripSet(MANDATORY, function (theme, prop) {
+ delete theme[prop];
+ }, function (after) {
+ do_check_eq(after, null);
+ });
+
+ roundtripSet(OPTIONAL, function (theme, prop) {
+ delete theme[prop];
+ }, function (after) {
+ do_check_neq(after, null);
+ });
+
+ roundtripSet(MANDATORY, function (theme, prop) {
+ theme[prop] = "";
+ }, function (after) {
+ do_check_eq(after, null);
+ });
+
+ roundtripSet(OPTIONAL, function (theme, prop) {
+ theme[prop] = "";
+ }, function (after, prop) {
+ do_check_eq(typeof after[prop], "undefined");
+ });
+
+ roundtripSet(MANDATORY, function (theme, prop) {
+ theme[prop] = " ";
+ }, function (after) {
+ do_check_eq(after, null);
+ });
+
+ roundtripSet(OPTIONAL, function (theme, prop) {
+ theme[prop] = " ";
+ }, function (after, prop) {
+ do_check_neq(after, null);
+ do_check_eq(typeof after[prop], "undefined");
+ });
+
+ function non_urls(props) {
+ return props.filter(prop => !/URL$/.test(prop));
+ }
+
+ function urls(props) {
+ return props.filter(prop => /URL$/.test(prop));
+ }
+
+ roundtripSet(non_urls(MANDATORY.concat(OPTIONAL)), function (theme, prop) {
+ theme[prop] = prop;
+ }, function (after, prop, before) {
+ do_check_eq(after[prop], before[prop]);
+ });
+
+ roundtripSet(non_urls(MANDATORY.concat(OPTIONAL)), function (theme, prop) {
+ theme[prop] = " " + prop + " ";
+ }, function (after, prop, before) {
+ do_check_eq(after[prop], before[prop].trim());
+ });
+
+ roundtripSet(urls(MANDATORY.concat(OPTIONAL)), function (theme, prop) {
+ theme[prop] = Math.random().toString();
+ }, function (after, prop, before) {
+ if (prop == "updateURL")
+ do_check_eq(typeof after[prop], "undefined");
+ else
+ do_check_eq(after[prop], "http://lwttest.invalid/" + before[prop]);
+ });
+
+ roundtripSet(urls(MANDATORY.concat(OPTIONAL)), function (theme, prop) {
+ theme[prop] = Math.random().toString();
+ }, function (after, prop, before) {
+ do_check_eq(after[prop], "https://lwttest.invalid/" + before[prop]);
+ }, true);
+
+ roundtripSet(urls(MANDATORY.concat(OPTIONAL)), function (theme, prop) {
+ theme[prop] = "https://sub.lwttest.invalid/" + Math.random().toString();
+ }, function (after, prop, before) {
+ do_check_eq(after[prop], before[prop]);
+ });
+
+ roundtripSet(urls(MANDATORY), function (theme, prop) {
+ theme[prop] = "ftp://lwttest.invalid/" + Math.random().toString();
+ }, function (after) {
+ do_check_eq(after, null);
+ });
+
+ roundtripSet(urls(OPTIONAL), function (theme, prop) {
+ theme[prop] = "ftp://lwttest.invalid/" + Math.random().toString();
+ }, function (after, prop) {
+ do_check_eq(typeof after[prop], "undefined");
+ });
+
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ data = dummy();
+ delete data.name;
+ try {
+ ltm.currentTheme = data;
+ do_throw("Should have rejected a theme with no name");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ data = dummy();
+ data.headerURL = "foo";
+ try {
+ ltm.currentTheme = data;
+ do_throw("Should have rejected a theme with a bad headerURL");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ data = dummy();
+ data.headerURL = "ftp://lwtest.invalid/test.png";
+ try {
+ ltm.currentTheme = data;
+ do_throw("Should have rejected a theme with a non-http(s) headerURL");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ data = dummy();
+ data.headerURL = "file:///test.png";
+ try {
+ ltm.currentTheme = data;
+ do_throw("Should have rejected a theme with a non-http(s) headerURL");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ data = dummy();
+ data.updateURL = "file:///test.json";
+ ltm.setLocalTheme(data);
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.updateURL, undefined);
+ ltm.forgetUsedTheme(ltm.currentTheme.id);
+ do_check_eq(ltm.usedThemes.length, 0);
+
+ data = dummy();
+ data.headerURL = "file:///test.png";
+ ltm.setLocalTheme(data);
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.currentTheme.headerURL, "file:///test.png");
+ ltm.forgetUsedTheme(ltm.currentTheme.id);
+ do_check_eq(ltm.usedThemes.length, 0);
+
+ data = dummy();
+ data.headerURL = "ftp://lwtest.invalid/test.png";
+ try {
+ ltm.setLocalTheme(data);
+ do_throw("Should have rejected a theme with a non-http(s), non-file headerURL");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ data = dummy();
+ delete data.id;
+ try {
+ ltm.currentTheme = data;
+ do_throw("Should have rejected a theme with no ID");
+ }
+ catch (e) {
+ // Expected exception
+ }
+
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ // Force the theme into the prefs anyway
+ let prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ let themes = [data];
+ prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+ do_check_eq(ltm.usedThemes.length, 1);
+
+ // This should silently drop the bad theme.
+ ltm.currentTheme = dummy();
+ do_check_eq(ltm.usedThemes.length, 1);
+ ltm.forgetUsedTheme(ltm.currentTheme.id);
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ // Add one broken and some working.
+ themes = [data, dummy("x1"), dummy("x2")];
+ prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+ do_check_eq(ltm.usedThemes.length, 3);
+
+ // Switching to an existing theme should drop the bad theme.
+ ltm.currentTheme = ltm.getUsedTheme("x1");
+ do_check_eq(ltm.usedThemes.length, 2);
+ ltm.forgetUsedTheme("x1");
+ ltm.forgetUsedTheme("x2");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+ do_check_eq(ltm.usedThemes.length, 3);
+
+ // Forgetting an existing theme should drop the bad theme.
+ ltm.forgetUsedTheme("x1");
+ do_check_eq(ltm.usedThemes.length, 1);
+ ltm.forgetUsedTheme("x2");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ // Test whether a JSON set with setCharPref can be retrieved with usedThemes
+ ltm.currentTheme = dummy("x0");
+ ltm.currentTheme = dummy("x1");
+ prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(ltm.usedThemes));
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.currentTheme.id, "x1");
+ do_check_eq(ltm.usedThemes[1].id, "x0");
+ do_check_eq(ltm.usedThemes[0].id, "x1");
+
+ ltm.forgetUsedTheme("x0");
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_neq(ltm.currentTheme, null);
+
+ ltm.forgetUsedTheme("x1");
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes");
+
+ ltm.currentTheme = dummy("x1");
+ ltm.currentTheme = dummy("x2");
+ ltm.currentTheme = dummy("x3");
+ ltm.currentTheme = dummy("x4");
+ ltm.currentTheme = dummy("x5");
+ ltm.currentTheme = dummy("x6");
+ ltm.currentTheme = dummy("x7");
+ ltm.currentTheme = dummy("x8");
+ ltm.currentTheme = dummy("x9");
+ ltm.currentTheme = dummy("x10");
+ ltm.currentTheme = dummy("x11");
+ ltm.currentTheme = dummy("x12");
+ ltm.currentTheme = dummy("x13");
+ ltm.currentTheme = dummy("x14");
+ ltm.currentTheme = dummy("x15");
+ ltm.currentTheme = dummy("x16");
+ ltm.currentTheme = dummy("x17");
+ ltm.currentTheme = dummy("x18");
+ ltm.currentTheme = dummy("x19");
+ ltm.currentTheme = dummy("x20");
+ ltm.currentTheme = dummy("x21");
+ ltm.currentTheme = dummy("x22");
+ ltm.currentTheme = dummy("x23");
+ ltm.currentTheme = dummy("x24");
+ ltm.currentTheme = dummy("x25");
+ ltm.currentTheme = dummy("x26");
+ ltm.currentTheme = dummy("x27");
+ ltm.currentTheme = dummy("x28");
+ ltm.currentTheme = dummy("x29");
+ ltm.currentTheme = dummy("x30");
+
+ do_check_eq(ltm.usedThemes.length, 30);
+
+ ltm.currentTheme = dummy("x31");
+
+ do_check_eq(ltm.usedThemes.length, 30);
+ do_check_eq(ltm.getUsedTheme("x1"), null);
+
+ Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 15);
+
+ do_check_eq(ltm.usedThemes.length, 15);
+
+ Services.prefs.setIntPref("lightweightThemes.maxUsedThemes", 32);
+
+ ltm.currentTheme = dummy("x1");
+ ltm.currentTheme = dummy("x2");
+ ltm.currentTheme = dummy("x3");
+ ltm.currentTheme = dummy("x4");
+ ltm.currentTheme = dummy("x5");
+ ltm.currentTheme = dummy("x6");
+ ltm.currentTheme = dummy("x7");
+ ltm.currentTheme = dummy("x8");
+ ltm.currentTheme = dummy("x9");
+ ltm.currentTheme = dummy("x10");
+ ltm.currentTheme = dummy("x11");
+ ltm.currentTheme = dummy("x12");
+ ltm.currentTheme = dummy("x13");
+ ltm.currentTheme = dummy("x14");
+ ltm.currentTheme = dummy("x15");
+ ltm.currentTheme = dummy("x16");
+
+ ltm.currentTheme = dummy("x32");
+
+ do_check_eq(ltm.usedThemes.length, 32);
+
+ ltm.currentTheme = dummy("x33");
+
+ do_check_eq(ltm.usedThemes.length, 32);
+
+ Services.prefs.clearUserPref("lightweightThemes.maxUsedThemes");
+
+ do_check_eq(ltm.usedThemes.length, 30);
+
+ let usedThemes = ltm.usedThemes;
+ for (let theme of usedThemes) {
+ ltm.forgetUsedTheme(theme.id);
+ }
+
+ // Check builtInTheme functionality for Bug 1094821
+ do_check_eq(ltm._builtInThemes.toString(), "[object Map]");
+ do_check_eq([...ltm._builtInThemes.entries()].length, 0);
+ do_check_eq(ltm.usedThemes.length, 0);
+
+ ltm.addBuiltInTheme(dummy("builtInTheme0"));
+ do_check_eq([...ltm._builtInThemes].length, 1);
+ do_check_eq(ltm.usedThemes.length, 1);
+ do_check_eq(ltm.usedThemes[0].id, "builtInTheme0");
+
+ ltm.addBuiltInTheme(dummy("builtInTheme1"));
+ do_check_eq([...ltm._builtInThemes].length, 2);
+ do_check_eq(ltm.usedThemes.length, 2);
+ do_check_eq(ltm.usedThemes[1].id, "builtInTheme1");
+
+ // Clear all and then re-add
+ ltm.clearBuiltInThemes();
+ do_check_eq([...ltm._builtInThemes].length, 0);
+ do_check_eq(ltm.usedThemes.length, 0);
+
+ ltm.addBuiltInTheme(dummy("builtInTheme0"));
+ ltm.addBuiltInTheme(dummy("builtInTheme1"));
+ do_check_eq([...ltm._builtInThemes].length, 2);
+ do_check_eq(ltm.usedThemes.length, 2);
+
+ do_test_pending();
+
+ AddonManager.getAddonByID("builtInTheme0@personas.mozilla.org", builtInThemeAddon => {
+ // App specific theme can't be uninstalled or disabled,
+ // but can be enabled (since it isn't already applied).
+ do_check_eq(hasPermission(builtInThemeAddon, "uninstall"), false);
+ do_check_eq(hasPermission(builtInThemeAddon, "disable"), false);
+ do_check_eq(hasPermission(builtInThemeAddon, "enable"), true);
+
+ ltm.currentTheme = dummy("x0");
+ do_check_eq([...ltm._builtInThemes].length, 2);
+ do_check_eq(ltm.usedThemes.length, 3);
+ do_check_eq(ltm.usedThemes[0].id, "x0");
+ do_check_eq(ltm.currentTheme.id, "x0");
+ do_check_eq(ltm.usedThemes[1].id, "builtInTheme0");
+ do_check_eq(ltm.usedThemes[2].id, "builtInTheme1");
+
+ Assert.throws(() => { ltm.addBuiltInTheme(dummy("builtInTheme0")) },
+ "Exception is thrown adding a duplicate theme");
+ Assert.throws(() => { ltm.addBuiltInTheme("not a theme object") },
+ "Exception is thrown adding an invalid theme");
+
+ AddonManager.getAddonByID("x0@personas.mozilla.org", x0Addon => {
+ // Currently applied (non-app-specific) can be uninstalled or disabled,
+ // but can't be enabled (since it's already applied).
+ do_check_eq(hasPermission(x0Addon, "uninstall"), true);
+ do_check_eq(hasPermission(x0Addon, "disable"), true);
+ do_check_eq(hasPermission(x0Addon, "enable"), false);
+
+ ltm.forgetUsedTheme("x0");
+ do_check_eq(ltm.currentTheme, null);
+
+ // Removing the currently applied app specific theme should unapply it
+ ltm.currentTheme = ltm.getUsedTheme("builtInTheme0");
+ do_check_eq(ltm.currentTheme.id, "builtInTheme0");
+ do_check_true(ltm.forgetBuiltInTheme("builtInTheme0"));
+ do_check_eq(ltm.currentTheme, null);
+
+ do_check_eq([...ltm._builtInThemes].length, 1);
+ do_check_eq(ltm.usedThemes.length, 1);
+
+ do_check_true(ltm.forgetBuiltInTheme("builtInTheme1"));
+ do_check_false(ltm.forgetBuiltInTheme("not-an-existing-theme-id"));
+
+ do_check_eq([...ltm._builtInThemes].length, 0);
+ do_check_eq(ltm.usedThemes.length, 0);
+ do_check_eq(ltm.currentTheme, null);
+
+ do_test_finished();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js
new file mode 100644
index 000000000..6c562db65
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_ProductAddonChecker.js
@@ -0,0 +1,244 @@
+"use strict";
+
+Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
+Components.utils.import("resource://testing-common/httpd.js");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+const LocalFile = new Components.Constructor("@mozilla.org/file/local;1", AM_Ci.nsIFile, "initWithPath");
+
+var testserver = new HttpServer();
+testserver.registerDirectory("/data/", do_get_file("data/productaddons"));
+testserver.start();
+var root = testserver.identity.primaryScheme + "://" +
+ testserver.identity.primaryHost + ":" +
+ testserver.identity.primaryPort + "/data/"
+
+/**
+ * Compares binary data of 2 arrays and returns true if they are the same
+ *
+ * @param arr1 The first array to compare
+ * @param arr2 The second array to compare
+*/
+function compareBinaryData(arr1, arr2) {
+ do_check_eq(arr1.length, arr2.length);
+ for (let i = 0; i < arr1.length; i++) {
+ if (arr1[i] != arr2[i]) {
+ do_print("Data differs at index " + i +
+ ", arr1: " + arr1[i] + ", arr2: " + arr2[i]);
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * Reads a file's data and returns it
+ *
+ * @param file The file to read the data from
+ * @return array of bytes for the data in the file.
+*/
+function getBinaryFileData(file) {
+ let fileStream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ // Open as RD_ONLY with default permissions.
+ fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+
+ let stream = AM_Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(AM_Ci.nsIBinaryInputStream);
+ stream.setInputStream(fileStream);
+ let bytes = stream.readByteArray(stream.available());
+ fileStream.close();
+ return bytes;
+}
+
+/**
+ * Compares binary data of 2 files and returns true if they are the same
+ *
+ * @param file1 The first file to compare
+ * @param file2 The second file to compare
+*/
+function compareFiles(file1, file2) {
+ return compareBinaryData(getBinaryFileData(file1), getBinaryFileData(file2));
+}
+
+add_task(function* test_404() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "404.xml");
+ do_check_true(res.usedFallback);
+});
+
+add_task(function* test_not_xml() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "bad.txt");
+ do_check_true(res.usedFallback);
+});
+
+add_task(function* test_invalid_xml() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "bad.xml");
+ do_check_true(res.usedFallback);
+});
+
+add_task(function* test_wrong_xml() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "bad2.xml");
+ do_check_true(res.usedFallback);
+});
+
+add_task(function* test_missing() {
+ let addons = yield ProductAddonChecker.getProductAddonList(root + "missing.xml");
+ do_check_eq(addons, null);
+});
+
+add_task(function* test_empty() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "empty.xml");
+ do_check_true(Array.isArray(res.gmpAddons));
+ do_check_eq(res.gmpAddons.length, 0);
+});
+
+add_task(function* test_good_xml() {
+ let res = yield ProductAddonChecker.getProductAddonList(root + "good.xml");
+ do_check_true(Array.isArray(res.gmpAddons));
+
+ // There are three valid entries in the XML
+ do_check_eq(res.gmpAddons.length, 5);
+
+ let addon = res.gmpAddons[0];
+ do_check_eq(addon.id, "test1");
+ do_check_eq(addon.URL, "http://example.com/test1.xpi");
+ do_check_eq(addon.hashFunction, undefined);
+ do_check_eq(addon.hashValue, undefined);
+ do_check_eq(addon.version, undefined);
+ do_check_eq(addon.size, undefined);
+
+ addon = res.gmpAddons[1];
+ do_check_eq(addon.id, "test2");
+ do_check_eq(addon.URL, "http://example.com/test2.xpi");
+ do_check_eq(addon.hashFunction, "md5");
+ do_check_eq(addon.hashValue, "djhfgsjdhf");
+ do_check_eq(addon.version, undefined);
+ do_check_eq(addon.size, undefined);
+
+ addon = res.gmpAddons[2];
+ do_check_eq(addon.id, "test3");
+ do_check_eq(addon.URL, "http://example.com/test3.xpi");
+ do_check_eq(addon.hashFunction, undefined);
+ do_check_eq(addon.hashValue, undefined);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.size, 45);
+
+ addon = res.gmpAddons[3];
+ do_check_eq(addon.id, "test4");
+ do_check_eq(addon.URL, undefined);
+ do_check_eq(addon.hashFunction, undefined);
+ do_check_eq(addon.hashValue, undefined);
+ do_check_eq(addon.version, undefined);
+ do_check_eq(addon.size, undefined);
+
+ addon = res.gmpAddons[4];
+ do_check_eq(addon.id, undefined);
+ do_check_eq(addon.URL, "http://example.com/test5.xpi");
+ do_check_eq(addon.hashFunction, undefined);
+ do_check_eq(addon.hashValue, undefined);
+ do_check_eq(addon.version, undefined);
+ do_check_eq(addon.size, undefined);
+});
+
+add_task(function* test_download_nourl() {
+ try {
+ let path = yield ProductAddonChecker.downloadAddon({});
+
+ yield OS.File.remove(path);
+ do_throw("Should not have downloaded a file with a missing url");
+ }
+ catch (e) {
+ do_check_true(true, "Should have thrown when downloading a file with a missing url.");
+ }
+});
+
+add_task(function* test_download_missing() {
+ try {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "nofile.xpi",
+ });
+
+ yield OS.File.remove(path);
+ do_throw("Should not have downloaded a missing file");
+ }
+ catch (e) {
+ do_check_true(true, "Should have thrown when downloading a missing file.");
+ }
+});
+
+add_task(function* test_download_noverify() {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "unsigned.xpi",
+ });
+
+ let stat = yield OS.File.stat(path);
+ do_check_false(stat.isDir);
+ do_check_eq(stat.size, 452)
+
+ do_check_true(compareFiles(do_get_file("data/productaddons/unsigned.xpi"), new LocalFile(path)));
+
+ yield OS.File.remove(path);
+});
+
+add_task(function* test_download_badsize() {
+ try {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "unsigned.xpi",
+ size: 400,
+ });
+
+ yield OS.File.remove(path);
+ do_throw("Should not have downloaded a file with a bad size");
+ }
+ catch (e) {
+ do_check_true(true, "Should have thrown when downloading a file with a bad size.");
+ }
+});
+
+add_task(function* test_download_badhashfn() {
+ try {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "unsigned.xpi",
+ hashFunction: "sha2567",
+ hashValue: "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
+ });
+
+ yield OS.File.remove(path);
+ do_throw("Should not have downloaded a file with a bad hash function");
+ }
+ catch (e) {
+ do_check_true(true, "Should have thrown when downloading a file with a bad hash function.");
+ }
+});
+
+add_task(function* test_download_badhash() {
+ try {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "unsigned.xpi",
+ hashFunction: "sha256",
+ hashValue: "8b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
+ });
+
+ yield OS.File.remove(path);
+ do_throw("Should not have downloaded a file with a bad hash");
+ }
+ catch (e) {
+ do_check_true(true, "Should have thrown when downloading a file with a bad hash.");
+ }
+});
+
+add_task(function* test_download_works() {
+ let path = yield ProductAddonChecker.downloadAddon({
+ URL: root + "unsigned.xpi",
+ size: 452,
+ hashFunction: "sha256",
+ hashValue: "9b9abf7ddfc1a6d7ffc7e0247481dcc202363e4445ad3494fb22036f1698c7f3",
+ });
+
+ let stat = yield OS.File.stat(path);
+ do_check_false(stat.isDir);
+
+ do_check_true(compareFiles(do_get_file("data/productaddons/unsigned.xpi"), new LocalFile(path)));
+
+ yield OS.File.remove(path);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
new file mode 100644
index 000000000..99ab8ab13
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIStates.js
@@ -0,0 +1,299 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test that we only check manifest age for disabled extensions
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+/* We want one add-on installed packed, and one installed unpacked
+ */
+
+function run_test() {
+ // Shut down the add-on manager after all tests run.
+ do_register_cleanup(promiseShutdownManager);
+ // Kick off the task-based tests...
+ run_next_test();
+}
+
+// Use bootstrap extensions so the changes will be immediate.
+// A packed extension, to be enabled
+writeInstallRDFToXPI({
+ id: "packed-enabled@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Packed, Enabled",
+}, profileDir);
+
+// Packed, will be disabled
+writeInstallRDFToXPI({
+ id: "packed-disabled@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Packed, Disabled",
+}, profileDir);
+
+// Unpacked, enabled
+writeInstallRDFToDir({
+ id: "unpacked-enabled@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Unpacked, Enabled",
+}, profileDir, undefined, "extraFile.js");
+
+
+// Unpacked, disabled
+writeInstallRDFToDir({
+ id: "unpacked-disabled@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Unpacked, disabled",
+}, profileDir, undefined, "extraFile.js");
+
+// Keep track of the last time stamp we've used, so that we can keep moving
+// it forward (if we touch two different files in the same add-on with the same
+// timestamp we may not consider the change significant)
+var lastTimestamp = Date.now();
+
+/*
+ * Helper function to touch a file and then test whether we detect the change.
+ * @param XS The XPIState object.
+ * @param aPath File path to touch.
+ * @param aChange True if we should notice the change, False if we shouldn't.
+ */
+function checkChange(XS, aPath, aChange) {
+ do_check_true(aPath.exists());
+ lastTimestamp += 10000;
+ do_print("Touching file " + aPath.path + " with " + lastTimestamp);
+ aPath.lastModifiedTime = lastTimestamp;
+ do_check_eq(XS.getInstallState(), aChange);
+ // Save the pref so we don't detect this change again
+ XS.save();
+}
+
+// Get a reference to the XPIState (loaded by startupManager) so we can unit test it.
+function getXS() {
+ let XPI = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+ return XPI.XPIStates;
+}
+
+add_task(function* detect_touches() {
+ startupManager();
+ let [pe, pd, ue, ud] = yield promiseAddonsByIDs([
+ "packed-enabled@tests.mozilla.org",
+ "packed-disabled@tests.mozilla.org",
+ "unpacked-enabled@tests.mozilla.org",
+ "unpacked-disabled@tests.mozilla.org"
+ ]);
+
+ do_print("Disable test add-ons");
+ pd.userDisabled = true;
+ ud.userDisabled = true;
+
+ let XS = getXS();
+
+ // Should be no changes detected here, because everything should start out up-to-date.
+ do_check_false(XS.getInstallState());
+
+ let states = XS.getLocation("app-profile");
+
+ // State should correctly reflect enabled/disabled
+ do_check_true(states.get("packed-enabled@tests.mozilla.org").enabled);
+ do_check_false(states.get("packed-disabled@tests.mozilla.org").enabled);
+ do_check_true(states.get("unpacked-enabled@tests.mozilla.org").enabled);
+ do_check_false(states.get("unpacked-disabled@tests.mozilla.org").enabled);
+
+ // Touch various files and make sure the change is detected.
+
+ // We notice that a packed XPI is touched for an enabled add-on.
+ let peFile = profileDir.clone();
+ peFile.append("packed-enabled@tests.mozilla.org.xpi");
+ checkChange(XS, peFile, true);
+
+ // We should notice the packed XPI change for a disabled add-on too.
+ let pdFile = profileDir.clone();
+ pdFile.append("packed-disabled@tests.mozilla.org.xpi");
+ checkChange(XS, pdFile, true);
+
+ // We notice changing install.rdf for an enabled unpacked add-on.
+ let ueDir = profileDir.clone();
+ ueDir.append("unpacked-enabled@tests.mozilla.org");
+ let manifest = ueDir.clone();
+ manifest.append("install.rdf");
+ checkChange(XS, manifest, true);
+ // We also notice changing another file for enabled unpacked add-on.
+ let otherFile = ueDir.clone();
+ otherFile.append("extraFile.js");
+ checkChange(XS, otherFile, true);
+
+ // We notice changing install.rdf for a *disabled* unpacked add-on.
+ let udDir = profileDir.clone();
+ udDir.append("unpacked-disabled@tests.mozilla.org");
+ manifest = udDir.clone();
+ manifest.append("install.rdf");
+ checkChange(XS, manifest, true);
+ // Finally, the case we actually care about...
+ // We *don't* notice changing another file for disabled unpacked add-on.
+ otherFile = udDir.clone();
+ otherFile.append("extraFile.js");
+ checkChange(XS, otherFile, false);
+
+ /*
+ * When we enable an unpacked add-on that was modified while it was
+ * disabled, we reflect the new timestamp in the add-on DB (otherwise, we'll
+ * think it changed on next restart).
+ */
+ ud.userDisabled = false;
+ let xState = XS.getAddon("app-profile", ud.id);
+ do_check_true(xState.enabled);
+ do_check_eq(xState.scanTime, ud.updateDate.getTime());
+});
+
+/*
+ * Uninstalling bootstrap add-ons should immediately remove them from the
+ * extensions.xpiState preference.
+ */
+add_task(function* uninstall_bootstrap() {
+ let [pe, pd, ue, ud] = yield promiseAddonsByIDs([
+ "packed-enabled@tests.mozilla.org",
+ "packed-disabled@tests.mozilla.org",
+ "unpacked-enabled@tests.mozilla.org",
+ "unpacked-disabled@tests.mozilla.org"
+ ]);
+ pe.uninstall();
+ let xpiState = Services.prefs.getCharPref("extensions.xpiState");
+ do_check_false(xpiState.includes("\"packed-enabled@tests.mozilla.org\""));
+});
+
+/*
+ * Installing a restartless add-on should immediately add it to XPIState
+ */
+add_task(function* install_bootstrap() {
+ let XS = getXS();
+
+ let installer = yield new Promise((resolve, reject) =>
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+
+ let promiseInstalled = new Promise((resolve, reject) => {
+ AddonManager.addInstallListener({
+ onInstallFailed: reject,
+ onInstallEnded: (install, newAddon) => resolve(newAddon)
+ });
+ });
+
+ installer.install();
+
+ let newAddon = yield promiseInstalled;
+ let xState = XS.getAddon("app-profile", newAddon.id);
+ do_check_true(!!xState);
+ do_check_true(xState.enabled);
+ do_check_eq(xState.scanTime, newAddon.updateDate.getTime());
+ newAddon.uninstall();
+});
+
+/*
+ * Installing an add-on that requires restart doesn't add to XPIState
+ * until after the restart; disable and enable happen immediately so that
+ * the next restart won't / will scan as necessary on the next restart,
+ * uninstalling it marks XPIState as disabled immediately
+ * and removes XPIState after restart.
+ */
+add_task(function* install_restart() {
+ let XS = getXS();
+
+ let installer = yield new Promise((resolve, reject) =>
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_4"), resolve));
+
+ let promiseInstalled = new Promise((resolve, reject) => {
+ AddonManager.addInstallListener({
+ onInstallFailed: reject,
+ onInstallEnded: (install, newAddon) => resolve(newAddon)
+ });
+ });
+
+ installer.install();
+
+ let newAddon = yield promiseInstalled;
+ let newID = newAddon.id;
+ let xState = XS.getAddon("app-profile", newID);
+ do_check_false(xState);
+
+ // Now we restart the add-on manager, and we need to get the XPIState again
+ // because the add-on manager reloads it.
+ XS = null;
+ newAddon = null;
+ yield promiseRestartManager();
+ XS = getXS();
+
+ newAddon = yield promiseAddonByID(newID);
+ xState = XS.getAddon("app-profile", newID);
+ do_check_true(xState);
+ do_check_true(xState.enabled);
+ do_check_eq(xState.scanTime, newAddon.updateDate.getTime());
+
+ // Check that XPIState enabled flag is updated immediately,
+ // and doesn't change over restart.
+ newAddon.userDisabled = true;
+ do_check_false(xState.enabled);
+ XS = null;
+ newAddon = null;
+ yield promiseRestartManager();
+ XS = getXS();
+ xState = XS.getAddon("app-profile", newID);
+ do_check_true(xState);
+ do_check_false(xState.enabled);
+
+ newAddon = yield promiseAddonByID(newID);
+ newAddon.userDisabled = false;
+ do_check_true(xState.enabled);
+ XS = null;
+ newAddon = null;
+ yield promiseRestartManager();
+ XS = getXS();
+ xState = XS.getAddon("app-profile", newID);
+ do_check_true(xState);
+ do_check_true(xState.enabled);
+
+ // Uninstalling immediately marks XPIState disabled,
+ // removes state after restart.
+ newAddon = yield promiseAddonByID(newID);
+ newAddon.uninstall();
+ xState = XS.getAddon("app-profile", newID);
+ do_check_true(xState);
+ do_check_false(xState.enabled);
+
+ // Restart to finish uninstall.
+ XS = null;
+ newAddon = null;
+ yield promiseRestartManager();
+ XS = getXS();
+ xState = XS.getAddon("app-profile", newID);
+ do_check_false(xState);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js
new file mode 100644
index 000000000..d733778a5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_XPIcancel.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test the cancellable doing/done/cancelAll API in XPIProvider
+
+var scope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+var XPIProvider = scope.XPIProvider;
+
+function run_test() {
+ // Check that cancelling with nothing in progress doesn't blow up
+ XPIProvider.cancelAll();
+
+ // Check that a basic object gets cancelled
+ let getsCancelled = {
+ isCancelled: false,
+ cancel: function () {
+ if (this.isCancelled)
+ do_throw("Already cancelled");
+ this.isCancelled = true;
+ }
+ };
+ XPIProvider.doing(getsCancelled);
+ XPIProvider.cancelAll();
+ do_check_true(getsCancelled.isCancelled);
+
+ // Check that if we complete a cancellable, it doesn't get cancelled
+ let doesntGetCancelled = {
+ cancel: () => do_throw("This should not have been cancelled")
+ };
+ XPIProvider.doing(doesntGetCancelled);
+ do_check_true(XPIProvider.done(doesntGetCancelled));
+ XPIProvider.cancelAll();
+
+ // A cancellable that adds a cancellable
+ getsCancelled.isCancelled = false;
+ let addsAnother = {
+ isCancelled: false,
+ cancel: function () {
+ if (this.isCancelled)
+ do_throw("Already cancelled");
+ this.isCancelled = true;
+ XPIProvider.doing(getsCancelled);
+ }
+ }
+ XPIProvider.doing(addsAnother);
+ XPIProvider.cancelAll();
+ do_check_true(addsAnother.isCancelled);
+ do_check_true(getsCancelled.isCancelled);
+
+ // A cancellable that removes another. This assumes that Set() iterates in the
+ // order that members were added
+ let removesAnother = {
+ isCancelled: false,
+ cancel: function () {
+ if (this.isCancelled)
+ do_throw("Already cancelled");
+ this.isCancelled = true;
+ XPIProvider.done(doesntGetCancelled);
+ }
+ }
+ XPIProvider.doing(removesAnother);
+ XPIProvider.doing(doesntGetCancelled);
+ XPIProvider.cancelAll();
+ do_check_true(removesAnother.isCancelled);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_addon_path_service.js b/toolkit/mozapps/extensions/test/xpcshell/test_addon_path_service.js
new file mode 100644
index 000000000..56ce3c614
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_addon_path_service.js
@@ -0,0 +1,38 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var service = Components.classes["@mozilla.org/addon-path-service;1"].getService(Components.interfaces.amIAddonPathService);
+
+function insert(path, value)
+{
+ service.insertPath("/test/" + path, value);
+}
+
+function find(path)
+{
+ return service.findAddonId("/test/" + path);
+}
+
+function run_test()
+{
+ insert("abc", "10");
+ insert("def", "11");
+ insert("axy", "12");
+ insert("defghij", "13");
+ insert("defghi", "14");
+
+ do_check_eq(find("abc"), "10");
+ do_check_eq(find("abc123"), "10");
+ do_check_eq(find("def"), "11");
+ do_check_eq(find("axy"), "12");
+ do_check_eq(find("axy1"), "12");
+ do_check_eq(find("defghij"), "13");
+ do_check_eq(find("abd"), "");
+ do_check_eq(find("x"), "");
+
+ insert("file:///home/billm/mozilla/in4/objdir-ff-dbg/dist/bin/browser/extensions/%7B972ce4c6-7e08-4474-a285-3208198ce6fd%7D/", "{972ce4c6-7e08-4474-a285-3208198ce6fd}");
+ insert("file:///home/billm/mozilla/addons/dl-helper-workspace/addon/", "{b9db16a4-6edc-47ec-a1f4-b86292ed211d}");
+
+ do_check_eq(find("file:///home/billm/mozilla/addons/dl-helper-workspace/addon/local/modules/medialist-manager.jsm"), "{b9db16a4-6edc-47ec-a1f4-b86292ed211d}");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js b/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
new file mode 100644
index 000000000..38e563979
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_asyncBlocklistLoad.js
@@ -0,0 +1,44 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService().wrappedJSObject;
+ let scope = Components.utils.import("resource://gre/modules/osfile.jsm");
+
+ // sync -> async
+ blocklist._loadBlocklist();
+ do_check_true(blocklist._isBlocklistLoaded());
+ yield blocklist._preloadBlocklist();
+ do_check_false(blocklist._isBlocklistPreloaded());
+ blocklist._clear();
+
+ // async -> sync
+ yield blocklist._preloadBlocklist();
+ do_check_false(blocklist._isBlocklistLoaded());
+ do_check_true(blocklist._isBlocklistPreloaded());
+ blocklist._loadBlocklist();
+ do_check_true(blocklist._isBlocklistLoaded());
+ do_check_false(blocklist._isBlocklistPreloaded());
+ blocklist._clear();
+
+ // async -> sync -> async
+ let read = scope.OS.File.read;
+ scope.OS.File.read = function(...args) {
+ return new Promise((resolve, reject) => {
+ do_execute_soon(() => {
+ blocklist._loadBlocklist();
+ resolve(read(...args));
+ });
+ });
+ }
+
+ yield blocklist._preloadBlocklist();
+ do_check_true(blocklist._isBlocklistLoaded());
+ do_check_false(blocklist._isBlocklistPreloaded());
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js b/toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js
new file mode 100644
index 000000000..3890b76e1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_backgroundupdate.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that background updates & notifications work as expected
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_backgroundupdate.rdf", testserver);
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+ startupManager();
+
+ do_test_pending();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+// Verify that with no add-ons installed the background update notifications get
+// called
+function run_test_1() {
+ AddonManager.getAddonsByTypes(["extension", "theme", "locale"], function(aAddons) {
+ do_check_eq(aAddons.length, 0);
+
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+ do_execute_soon(run_test_2);
+ }, "addons-background-update-complete", false);
+
+ // Trigger the background update timer handler
+ gInternalManager.notify(null);
+ });
+}
+
+// Verify that with two add-ons installed both of which claim to have updates
+// available we get the notification after both updates attempted to start
+function run_test_2() {
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_backgroundupdate.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_backgroundupdate.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 2",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ // Background update uses a different pref, if set
+ Services.prefs.setCharPref("extensions.update.background.url",
+ "http://localhost:" + gPort +"/data/test_backgroundupdate.rdf");
+ restartManager();
+
+ // Do hotfix checks
+ Services.prefs.setCharPref("extensions.hotfix.id", "hotfix@tests.mozilla.org");
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" + gPort + "/missing.rdf");
+
+ let installCount = 0;
+ let completeCount = 0;
+ let sawCompleteNotification = false;
+
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+
+ do_check_eq(installCount, 3);
+ sawCompleteNotification = true;
+ }, "addons-background-update-complete", false);
+
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ installCount++;
+ },
+
+ onDownloadFailed: function(aInstall) {
+ completeCount++;
+ if (completeCount == 3) {
+ do_check_true(sawCompleteNotification);
+ end_test();
+ }
+ }
+ });
+
+ // Trigger the background update timer handler
+ gInternalManager.notify(null);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js b/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js
new file mode 100644
index 000000000..d3ccf68f3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bad_json.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that we rebuild the database correctly if it contains
+// JSON data that parses correctly but doesn't contain required fields
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending("Bad JSON");
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // This addon will be auto-installed at startup
+ writeInstallRDFForExtension(addon1, profileDir);
+
+ startupManager();
+
+ shutdownManager();
+
+ // First startup/shutdown finished
+ // Replace the JSON store with something bogus
+ saveJSON({not: "what we expect to find"}, gExtensionsJSON);
+
+ startupManager(false);
+ // Retrieve an addon to force the database to rebuild
+ AddonManager.getAddonsByIDs([addon1.id], callback_soon(after_db_rebuild));
+}
+
+function after_db_rebuild([a1]) {
+ do_check_eq(a1.id, addon1.id);
+
+ shutdownManager();
+
+ // Make sure our JSON database has schemaVersion and our installed extension
+ let data = loadJSON(gExtensionsJSON);
+ do_check_true("schemaVersion" in data);
+ do_check_eq(data.addons[0].id, addon1.id);
+
+ do_test_finished("Bad JSON");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js b/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js
new file mode 100644
index 000000000..6ebf088d6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_badschema.js
@@ -0,0 +1,404 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we rebuild something sensible from a database with a bad schema
+
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_corrupt.rdf", testserver);
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+// Will be enabled
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will get a compatibility update and be enabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Will get a compatibility update and be disabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Stays incompatible
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Enabled bootstrapped
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Disabled bootstrapped
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The default theme
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The selected theme
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Create and configure the HTTP server.
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a2, a3, a4,
+ a7, t2]) {
+ // Set up the initial state
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ a7.userDisabled = true;
+ t2.userDisabled = false;
+ a3.findUpdates({
+ onUpdateFinished: function() {
+ a4.findUpdates({
+ onUpdateFinished: function() {
+ do_execute_soon(run_test_1);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ });
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test_1() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a1, a2, a3,
+ a4, a5, a6,
+ a7, t1, t2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_execute_soon(run_test_1_modified_db);
+ });
+}
+
+
+function run_test_1_modified_db() {
+ // After restarting the database won't be open so we can alter
+ // the schema
+ shutdownManager();
+ changeXPIDBVersion(100);
+ startupManager();
+
+ // Accessing the add-ons should open and recover the database. Since
+ // migration occurs everything should be recovered correctly
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a1, a2, a3,
+ a4, a5, a6,
+ a7, t1, t2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_execute_soon(run_test_1_after_rebuild);
+ });
+}
+
+function run_test_1_after_rebuild() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a1, a2, a3,
+ a4, a5, a6,
+ a7, t1, t2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_gfx.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_gfx.js
new file mode 100644
index 000000000..cbcd5cb7e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_gfx.js
@@ -0,0 +1,157 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const TEST_APP_ID = "xpcshell@tests.mozilla.org";
+
+
+const EVENT_NAME = "blocklist-data-gfxItems";
+
+const SAMPLE_GFX_RECORD = {
+ "driverVersionComparator": "LESS_THAN_OR_EQUAL",
+ "driverVersion": "8.17.12.5896",
+ "vendor": "0x10de",
+ "blockID": "g36",
+ "feature": "DIRECT3D_9_LAYERS",
+ "devices": ["0x0a6c", "geforce"],
+ "featureStatus": "BLOCKED_DRIVER_VERSION",
+ "last_modified": 1458035931837,
+ "os": "WINNT 6.1",
+ "id": "3f947f16-37c2-4e96-d356-78b26363729b",
+ "versionRange": {"minVersion": 0, "maxVersion": "*"}
+};
+
+
+function Blocklist() {
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService().wrappedJSObject;
+ blocklist._clear();
+ return blocklist;
+}
+
+
+function run_test() {
+ run_next_test();
+}
+
+
+add_task(function* test_sends_serialized_data() {
+ const blocklist = Blocklist();
+ blocklist._gfxEntries = [SAMPLE_GFX_RECORD];
+
+ const expected = "blockID:g36\tdevices:0x0a6c,geforce\tdriverVersion:8.17.12.5896\t" +
+ "driverVersionComparator:LESS_THAN_OR_EQUAL\tfeature:DIRECT3D_9_LAYERS\t" +
+ "featureStatus:BLOCKED_DRIVER_VERSION\tos:WINNT 6.1\tvendor:0x10de\t" +
+ "versionRange:0,*";
+ let received;
+ const observe = (subject, topic, data) => { received = data };
+ Services.obs.addObserver(observe, EVENT_NAME, false);
+ blocklist._notifyObserversBlocklistGFX();
+ equal(received, expected);
+ Services.obs.removeObserver(observe, EVENT_NAME);
+});
+
+
+add_task(function* test_parsing_fails_if_devices_contains_comma() {
+ const input = "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
+ "<gfxItems>" +
+ " <gfxBlacklistEntry>" +
+ " <devices>" +
+ " <device>0x2,582</device>" +
+ " <device>0x2782</device>" +
+ " </devices>" +
+ " </gfxBlacklistEntry>" +
+ "</gfxItems>" +
+ "</blocklist>";
+ const blocklist = Blocklist();
+ blocklist._loadBlocklistFromString(input);
+ equal(blocklist._gfxEntries[0].devices.length, 1);
+ equal(blocklist._gfxEntries[0].devices[0], "0x2782");
+});
+
+
+add_task(function* test_empty_values_are_ignored() {
+ const input = "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
+ "<gfxItems>" +
+ " <gfxBlacklistEntry>" +
+ " <os></os>" +
+ " </gfxBlacklistEntry>" +
+ "</gfxItems>" +
+ "</blocklist>";
+ const blocklist = Blocklist();
+ let received;
+ const observe = (subject, topic, data) => { received = data };
+ Services.obs.addObserver(observe, EVENT_NAME, false);
+ blocklist._loadBlocklistFromString(input);
+ ok(received.indexOf("os" < 0));
+ Services.obs.removeObserver(observe, EVENT_NAME);
+});
+
+add_task(function* test_empty_devices_are_ignored() {
+ const input = "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
+ "<gfxItems>" +
+ " <gfxBlacklistEntry>" +
+ " <devices></devices>" +
+ " </gfxBlacklistEntry>" +
+ "</gfxItems>" +
+ "</blocklist>";
+ const blocklist = Blocklist();
+ let received;
+ const observe = (subject, topic, data) => { received = data };
+ Services.obs.addObserver(observe, EVENT_NAME, false);
+ blocklist._loadBlocklistFromString(input);
+ ok(received.indexOf("devices" < 0));
+ Services.obs.removeObserver(observe, EVENT_NAME);
+});
+
+add_task(function* test_version_range_default_values() {
+ const input = "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
+ "<gfxItems>" +
+ " <gfxBlacklistEntry>" +
+ " <versionRange minVersion=\"13.0b2\" maxVersion=\"42.0\"/>" +
+ " </gfxBlacklistEntry>" +
+ " <gfxBlacklistEntry>" +
+ " <versionRange maxVersion=\"2.0\"/>" +
+ " </gfxBlacklistEntry>" +
+ " <gfxBlacklistEntry>" +
+ " <versionRange minVersion=\"1.0\"/>" +
+ " </gfxBlacklistEntry>" +
+ " <gfxBlacklistEntry>" +
+ " <versionRange minVersion=\" \"/>" +
+ " </gfxBlacklistEntry>" +
+ " <gfxBlacklistEntry>" +
+ " <versionRange/>" +
+ " </gfxBlacklistEntry>" +
+ "</gfxItems>" +
+ "</blocklist>";
+ const blocklist = Blocklist();
+ blocklist._loadBlocklistFromString(input);
+ equal(blocklist._gfxEntries[0].versionRange.minVersion, "13.0b2");
+ equal(blocklist._gfxEntries[0].versionRange.maxVersion, "42.0");
+ equal(blocklist._gfxEntries[1].versionRange.minVersion, "0");
+ equal(blocklist._gfxEntries[1].versionRange.maxVersion, "2.0");
+ equal(blocklist._gfxEntries[2].versionRange.minVersion, "1.0");
+ equal(blocklist._gfxEntries[2].versionRange.maxVersion, "*");
+ equal(blocklist._gfxEntries[3].versionRange.minVersion, "0");
+ equal(blocklist._gfxEntries[3].versionRange.maxVersion, "*");
+ equal(blocklist._gfxEntries[4].versionRange.minVersion, "0");
+ equal(blocklist._gfxEntries[4].versionRange.maxVersion, "*");
+});
+
+add_task(function* test_blockid_attribute() {
+ const input = "<blocklist xmlns=\"http://www.mozilla.org/2006/addons-blocklist\">" +
+ "<gfxItems>" +
+ " <gfxBlacklistEntry blockID=\"g60\">" +
+ " <vendor> 0x10de </vendor>" +
+ " </gfxBlacklistEntry>" +
+ " <gfxBlacklistEntry>" +
+ " <feature> DIRECT3D_9_LAYERS </feature>" +
+ " </gfxBlacklistEntry>" +
+ "</gfxItems>" +
+ "</blocklist>";
+ const blocklist = Blocklist();
+ blocklist._loadBlocklistFromString(input);
+ equal(blocklist._gfxEntries[0].blockID, "g60");
+ ok(!blocklist._gfxEntries[1].hasOwnProperty("blockID"));
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_metadata_filters.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_metadata_filters.js
new file mode 100644
index 000000000..5befa6fa0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_metadata_filters.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests blocking of extensions by ID, name, creator, homepageURL, updateURL
+// and RegExps for each. See bug 897735.
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_blocklist_metadata_filters_1.xml", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = args.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function load_blocklist(aFile, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ // Should get blocked by name
+ writeInstallRDFForExtension({
+ id: "block1@tests.mozilla.org",
+ version: "1.0",
+ name: "Mozilla Corp.",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ // Should get blocked by all the attributes.
+ writeInstallRDFForExtension({
+ id: "block2@tests.mozilla.org",
+ version: "1.0",
+ name: "Moz-addon",
+ creator: "Dangerous",
+ homepageURL: "www.extension.dangerous.com",
+ updateURL: "www.extension.dangerous.com/update.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ // Fails to get blocked because of a different ID even though other
+ // attributes match against a blocklist entry.
+ writeInstallRDFForExtension({
+ id: "block3@tests.mozilla.org",
+ version: "1.0",
+ name: "Moz-addon",
+ creator: "Dangerous",
+ homepageURL: "www.extensions.dangerous.com",
+ updateURL: "www.extension.dangerous.com/update.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org",
+ "block2@tests.mozilla.org",
+ "block3@tests.mozilla.org"], function([a1, a2, a3]) {
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(a2.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(a3.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ load_blocklist("test_blocklist_metadata_filters_1.xml", function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org",
+ "block2@tests.mozilla.org",
+ "block3@tests.mozilla.org"], function([a1, a2, a3]) {
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ do_check_eq(a2.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(a3.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_prefs.js
new file mode 100644
index 000000000..41ef62f98
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_prefs.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests resetting of preferences in blocklist entry when an add-on is blocked.
+// See bug 802434.
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+XPCOMUtils.defineLazyGetter(this, "gPref", function bls_gPref() {
+ return Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefService).
+ QueryInterface(Ci.nsIPrefBranch);
+});
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_blocklist_prefs_1.xml", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// A window watcher to handle the blocklist UI.
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = args.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function load_blocklist(aFile, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ // Add 2 extensions
+ writeInstallRDFForExtension({
+ id: "block1@tests.mozilla.org",
+ version: "1.0",
+ name: "Blocked add-on-1 with to-be-reset prefs",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "block2@tests.mozilla.org",
+ version: "1.0",
+ name: "Blocked add-on-2 with to-be-reset prefs",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ // Pre-set the preferences that we expect to get reset.
+ gPref.setIntPref("test.blocklist.pref1", 15);
+ gPref.setIntPref("test.blocklist.pref2", 15);
+ gPref.setBoolPref("test.blocklist.pref3", true);
+ gPref.setBoolPref("test.blocklist.pref4", true);
+
+ startupManager();
+
+ // Before blocklist is loaded.
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org",
+ "block2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(a2.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ do_check_eq(gPref.getIntPref("test.blocklist.pref1"), 15);
+ do_check_eq(gPref.getIntPref("test.blocklist.pref2"), 15);
+ do_check_eq(gPref.getBoolPref("test.blocklist.pref3"), true);
+ do_check_eq(gPref.getBoolPref("test.blocklist.pref4"), true);
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ load_blocklist("test_blocklist_prefs_1.xml", function() {
+ restartManager();
+
+ // Blocklist changes should have applied and the prefs must be reset.
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org",
+ "block2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ do_check_neq(a2, null);
+ do_check_eq(a2.blocklistState, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ // All these prefs must be reset to defaults.
+ do_check_eq(gPref.prefHasUserValue("test.blocklist.pref1"), false);
+ do_check_eq(gPref.prefHasUserValue("test.blocklist.pref2"), false);
+ do_check_eq(gPref.prefHasUserValue("test.blocklist.pref3"), false);
+ do_check_eq(gPref.prefHasUserValue("test.blocklist.pref4"), false);
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js
new file mode 100644
index 000000000..6e664adae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklist_regexp.js
@@ -0,0 +1,120 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that blocklist entries using RegExp work as expected. This only covers
+// behavior specific to RegExp entries - general behavior is already tested
+// in test_blocklistchange.js.
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_blocklist_regexp_1.xml", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = args.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+
+function load_blocklist(aFile, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ // if we're not using the blocklist.xml for certificate blocklist state,
+ // ensure that kinto update is enabled
+ if (!Services.prefs.getBoolPref("security.onecrl.via.amo")) {
+ ok(Services.prefs.getBoolPref("services.blocklist.update_enabled", false),
+ "Kinto update should be enabled");
+ }
+ blocklist.notify(null);
+}
+
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ writeInstallRDFForExtension({
+ id: "block1@tests.mozilla.org",
+ version: "1.0",
+ name: "RegExp blocked add-on",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org"], function([a1]) {
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ load_blocklist("test_blocklist_regexp_1.xml", function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["block1@tests.mozilla.org"], function([a1]) {
+ // Blocklist contains two entries that will match this addon - ensure
+ // that the first one is applied.
+ do_check_neq(a1, null);
+ do_check_eq(a1.blocklistState, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js b/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
new file mode 100644
index 000000000..d065f700d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_blocklistchange.js
@@ -0,0 +1,1305 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that changes that cause an add-on to become unblocked or blocked have
+// the right effect
+
+// The tests follow a mostly common pattern. First they start with the add-ons
+// unblocked, then they make a change that causes the add-ons to become blocked
+// then they make a similar change that keeps the add-ons blocked then they make
+// a change that unblocks the add-ons. Some tests skip the initial part and
+// start with add-ons detected as blocked.
+
+// softblock1 is enabled/disabled by the blocklist changes so its softDisabled
+// property should always match its userDisabled property
+
+// softblock2 gets manually enabled then disabled after it becomes blocked so
+// its softDisabled property should never become true after that
+
+// softblock3 does the same as softblock2 however it remains disabled
+
+// softblock4 is disabled while unblocked and so should never have softDisabled
+// set to true and stay userDisabled. This add-on is not used in tests that
+// start with add-ons blocked as it would be identical to softblock3
+
+// softblock5 is a theme. Currently themes just get disabled when they become
+// softblocked and have to be manually re-enabled if they become completely
+// unblocked (bug 657520)
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+// Allow insecure updates
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false)
+
+var testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/blocklistchange/addon_update1.rdf", testserver);
+mapFile("/data/blocklistchange/addon_update2.rdf", testserver);
+mapFile("/data/blocklistchange/addon_update3.rdf", testserver);
+mapFile("/data/blocklistchange/addon_change.xml", testserver);
+mapFile("/data/blocklistchange/app_update.xml", testserver);
+mapFile("/data/blocklistchange/blocklist_update1.xml", testserver);
+mapFile("/data/blocklistchange/blocklist_update2.xml", testserver);
+mapFile("/data/blocklistchange/manual_update.xml", testserver);
+
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+
+var default_theme = {
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock1_1 = {
+ id: "softblock1@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock1_2 = {
+ id: "softblock1@tests.mozilla.org",
+ version: "2.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock1_3 = {
+ id: "softblock1@tests.mozilla.org",
+ version: "3.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock2_1 = {
+ id: "softblock2@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock2_2 = {
+ id: "softblock2@tests.mozilla.org",
+ version: "2.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock2_3 = {
+ id: "softblock2@tests.mozilla.org",
+ version: "3.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock3_1 = {
+ id: "softblock3@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock3_2 = {
+ id: "softblock3@tests.mozilla.org",
+ version: "2.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock3_3 = {
+ id: "softblock3@tests.mozilla.org",
+ version: "3.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock4_1 = {
+ id: "softblock4@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock4_2 = {
+ id: "softblock4@tests.mozilla.org",
+ version: "2.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock4_3 = {
+ id: "softblock4@tests.mozilla.org",
+ version: "3.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock5_1 = {
+ id: "softblock5@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock5_2 = {
+ id: "softblock5@tests.mozilla.org",
+ version: "2.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var softblock5_3 = {
+ id: "softblock5@tests.mozilla.org",
+ version: "3.0",
+ name: "Softblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var hardblock_1 = {
+ id: "hardblock@tests.mozilla.org",
+ version: "1.0",
+ name: "Hardblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var hardblock_2 = {
+ id: "hardblock@tests.mozilla.org",
+ version: "2.0",
+ name: "Hardblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var hardblock_3 = {
+ id: "hardblock@tests.mozilla.org",
+ version: "3.0",
+ name: "Hardblocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var regexpblock_1 = {
+ id: "regexpblock@tests.mozilla.org",
+ version: "1.0",
+ name: "RegExp-blocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update1.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var regexpblock_2 = {
+ id: "regexpblock@tests.mozilla.org",
+ version: "2.0",
+ name: "RegExp-blocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update2.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+var regexpblock_3 = {
+ id: "regexpblock@tests.mozilla.org",
+ version: "3.0",
+ name: "RegExp-blocked add-on",
+ updateURL: "http://localhost:" + gPort + "/data/blocklistchange/addon_update3.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+};
+
+const ADDON_IDS = ["softblock1@tests.mozilla.org",
+ "softblock2@tests.mozilla.org",
+ "softblock3@tests.mozilla.org",
+ "softblock4@tests.mozilla.org",
+ "softblock5@tests.mozilla.org",
+ "hardblock@tests.mozilla.org",
+ "regexpblock@tests.mozilla.org"];
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, openArgs) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = openArgs.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+var InstallConfirm = {
+ confirm: function(aWindow, aUrl, aInstalls, aInstallCount) {
+ aInstalls.forEach(function(aInstall) {
+ aInstall.install();
+ });
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.amIWebInstallPrompt)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+var InstallConfirmFactory = {
+ createInstance: function createInstance(outer, iid) {
+ if (outer != null)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return InstallConfirm.QueryInterface(iid);
+ }
+};
+
+var registrar = Components.manager.QueryInterface(Components.interfaces.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{f0863905-4dde-42e2-991c-2dc8209bc9ca}"),
+ "Fake Install Prompt",
+ "@mozilla.org/addons/web-install-prompt;1", InstallConfirmFactory);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function Pload_blocklist(aFile) {
+ let blocklist_updated = new Promise((resolve, reject) => {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ resolve();
+ }, "blocklist-updated", false);
+ });
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/blocklistchange/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+ return blocklist_updated;
+}
+
+// Does a background update check for add-ons and returns a promise that
+// resolves when any started installs complete
+function Pbackground_update() {
+ var installCount = 0;
+ var backgroundCheckCompleted = false;
+
+ let updated = new Promise((resolve, reject) => {
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ installCount++;
+ },
+
+ onInstallEnded: function(aInstall) {
+ installCount--;
+ // Wait until all started installs have completed
+ if (installCount)
+ return;
+
+ AddonManager.removeInstallListener(this);
+
+ // If the background check hasn't yet completed then let that call the
+ // callback when it is done
+ if (!backgroundCheckCompleted)
+ return;
+
+ resolve();
+ }
+ })
+
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+ backgroundCheckCompleted = true;
+
+ // If any new installs have started then we'll call the callback once they
+ // are completed
+ if (installCount)
+ return;
+
+ resolve();
+ }, "addons-background-update-complete", false);
+ });
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+ return updated;
+}
+
+// Manually updates the test add-ons to the given version
+function Pmanual_update(aVersion) {
+ let Pinstalls = [];
+ for (let name of ["soft1", "soft2", "soft3", "soft4", "soft5", "hard1", "regexp1"]) {
+ Pinstalls.push(new Promise((resolve, reject) => {
+ AddonManager.getInstallForURL("http://localhost:" + gPort + "/addons/blocklist_"
+ + name + "_" + aVersion + ".xpi",
+ resolve, "application/x-xpinstall");
+ }));
+ }
+
+ return Promise.all(Pinstalls).then(installs => {
+ let completePromises = [];
+ for (let install of installs) {
+ completePromises.push(new Promise(resolve => {
+ install.addListener({
+ onDownloadCancelled: resolve,
+ onInstallEnded: resolve
+ })
+ }));
+ }
+
+ // Use the default web installer to cancel/allow installs based on whether
+ // the add-on is valid or not.
+ let webInstaller = Cc["@mozilla.org/addons/web-install-listener;1"]
+ .getService(Ci.amIWebInstallListener);
+ webInstaller.onWebInstallRequested(null, null, installs);
+
+ return Promise.all(completePromises);
+ });
+}
+
+// Checks that an add-ons properties match expected values
+function check_addon(aAddon, aExpectedVersion, aExpectedUserDisabled,
+ aExpectedSoftDisabled, aExpectedState) {
+ do_check_neq(aAddon, null);
+ do_print("Testing " + aAddon.id + " version " + aAddon.version + " user "
+ + aAddon.userDisabled + " soft " + aAddon.softDisabled
+ + " perms " + aAddon.permissions);
+
+ do_check_eq(aAddon.version, aExpectedVersion);
+ do_check_eq(aAddon.blocklistState, aExpectedState);
+ do_check_eq(aAddon.userDisabled, aExpectedUserDisabled);
+ do_check_eq(aAddon.softDisabled, aExpectedSoftDisabled);
+ if (aAddon.softDisabled)
+ do_check_true(aAddon.userDisabled);
+
+ if (aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ do_print("blocked, PERM_CAN_ENABLE " + aAddon.id);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_print("blocked, PERM_CAN_DISABLE " + aAddon.id);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ }
+ else if (aAddon.userDisabled) {
+ do_print("userDisabled, PERM_CAN_ENABLE " + aAddon.id);
+ do_check_true(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_print("userDisabled, PERM_CAN_DISABLE " + aAddon.id);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ }
+ else {
+ do_print("other, PERM_CAN_ENABLE " + aAddon.id);
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ if (aAddon.type != "theme") {
+ do_print("other, PERM_CAN_DISABLE " + aAddon.id);
+ do_check_true(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_DISABLE));
+ }
+ }
+ do_check_eq(aAddon.appDisabled, aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ let willBeActive = aAddon.isActive;
+ if (hasFlag(aAddon.pendingOperations, AddonManager.PENDING_DISABLE))
+ willBeActive = false;
+ else if (hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE))
+ willBeActive = true;
+
+ if (aExpectedUserDisabled || aExpectedState == Ci.nsIBlocklistService.STATE_BLOCKED) {
+ do_check_false(willBeActive);
+ }
+ else {
+ do_check_true(willBeActive);
+ }
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ run_next_test();
+}
+
+add_task(function* init() {
+ writeInstallRDFForExtension(default_theme, profileDir);
+ writeInstallRDFForExtension(softblock1_1, profileDir);
+ writeInstallRDFForExtension(softblock2_1, profileDir);
+ writeInstallRDFForExtension(softblock3_1, profileDir);
+ writeInstallRDFForExtension(softblock4_1, profileDir);
+ writeInstallRDFForExtension(softblock5_1, profileDir);
+ writeInstallRDFForExtension(hardblock_1, profileDir);
+ writeInstallRDFForExtension(regexpblock_1, profileDir);
+ startupManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+ s4.userDisabled = true;
+ s5.userDisabled = false;
+});
+
+// Starts with add-ons unblocked and then switches application versions to
+// change add-ons to blocked and back
+add_task(function* run_app_update_test() {
+ do_print("Test: " + arguments.callee.name);
+ yield promiseRestartManager();
+ yield Pload_blocklist("app_update.xml");
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "test/1.0");
+});
+
+add_task(function* app_update_step_2() {
+ yield promiseRestartManager("2");
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+});
+
+add_task(function* app_update_step_3() {
+ yield promiseRestartManager();
+
+ yield promiseRestartManager("2.5");
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+});
+
+add_task(function* app_update_step_4() {
+ yield promiseRestartManager("1");
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s5.userDisabled = false;
+});
+
+// Starts with add-ons unblocked and then switches application versions to
+// change add-ons to blocked and back. A DB schema change is faked to force a
+// rebuild when the application version changes
+add_task(function* run_app_update_schema_test() {
+ do_print("Test: " + arguments.callee.name);
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "test/1.0");
+});
+
+add_task(function* update_schema_2() {
+ yield promiseShutdownManager();
+
+ changeXPIDBVersion(100);
+ gAppInfo.version = "2";
+ startupManager(true);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+});
+
+add_task(function* update_schema_3() {
+ yield promiseRestartManager();
+
+ yield promiseShutdownManager();
+ changeXPIDBVersion(100);
+ gAppInfo.version = "2.5";
+ startupManager(true);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+});
+
+add_task(function* update_schema_4() {
+ yield promiseShutdownManager();
+
+ changeXPIDBVersion(100);
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+});
+
+add_task(function* update_schema_5() {
+ yield promiseShutdownManager();
+
+ changeXPIDBVersion(100);
+ gAppInfo.version = "1";
+ startupManager(true);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s5.userDisabled = false;
+});
+
+// Starts with add-ons unblocked and then loads new blocklists to change add-ons
+// to blocked and back again.
+add_task(function* run_blocklist_update_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield Pload_blocklist("blocklist_update1.xml");
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "test/1.0");
+
+ yield Pload_blocklist("blocklist_update2.xml");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+
+ yield promiseRestartManager();
+
+ yield Pload_blocklist("blocklist_update2.xml");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ yield Pload_blocklist("blocklist_update1.xml");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s5.userDisabled = false;
+});
+
+// Starts with add-ons unblocked and then new versions are installed outside of
+// the app to change them to blocked and back again.
+add_task(function* run_addon_change_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield Pload_blocklist("addon_change.xml");
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "test/1.0");
+});
+
+add_task(function* run_addon_change_2() {
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock1_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock2_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock2_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock3_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock3_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock4_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock4_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock5_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock5_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(hardblock_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, hardblock_2.id), Date.now() + 10000);
+ writeInstallRDFForExtension(regexpblock_2, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, regexpblock_2.id), Date.now() + 10000);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+});
+
+add_task(function* run_addon_change_3() {
+ yield promiseRestartManager();
+
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock1_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock2_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock2_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock3_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock3_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock4_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock4_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock5_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock5_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(hardblock_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, hardblock_3.id), Date.now() + 20000);
+ writeInstallRDFForExtension(regexpblock_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, regexpblock_3.id), Date.now() + 20000);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+});
+
+add_task(function* run_addon_change_4() {
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock1_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(softblock2_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock2_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(softblock3_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock3_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(softblock4_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock4_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(softblock5_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock5_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(hardblock_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, hardblock_1.id), Date.now() + 30000);
+ writeInstallRDFForExtension(regexpblock_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, regexpblock_1.id), Date.now() + 30000);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ do_check_eq(Services.prefs.getCharPref("general.skins.selectedSkin"), "classic/1.0");
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s5.userDisabled = false;
+});
+
+// Starts with add-ons blocked and then new versions are installed outside of
+// the app to change them to unblocked.
+add_task(function* run_addon_change_2_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseShutdownManager();
+
+ getFileForAddon(profileDir, softblock1_1.id).remove(true);
+ getFileForAddon(profileDir, softblock2_1.id).remove(true);
+ getFileForAddon(profileDir, softblock3_1.id).remove(true);
+ getFileForAddon(profileDir, softblock4_1.id).remove(true);
+ getFileForAddon(profileDir, softblock5_1.id).remove(true);
+ getFileForAddon(profileDir, hardblock_1.id).remove(true);
+ getFileForAddon(profileDir, regexpblock_1.id).remove(true);
+
+ startupManager(false);
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_2, profileDir);
+ writeInstallRDFForExtension(softblock2_2, profileDir);
+ writeInstallRDFForExtension(softblock3_2, profileDir);
+ writeInstallRDFForExtension(softblock4_2, profileDir);
+ writeInstallRDFForExtension(softblock5_2, profileDir);
+ writeInstallRDFForExtension(hardblock_2, profileDir);
+ writeInstallRDFForExtension(regexpblock_2, profileDir);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "2.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+});
+
+add_task(function* addon_change_2_test_2() {
+ yield promiseRestartManager();
+
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock1_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock2_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock2_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock3_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock3_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock4_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock4_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(softblock5_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock5_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(hardblock_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, hardblock_3.id), Date.now() + 10000);
+ writeInstallRDFForExtension(regexpblock_3, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, regexpblock_3.id), Date.now() + 10000);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+});
+
+add_task(function* addon_change_2_test_3() {
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock1_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock2_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock2_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock3_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock3_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock4_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock4_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(softblock5_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, softblock5_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(hardblock_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, hardblock_1.id), Date.now() + 20000);
+ writeInstallRDFForExtension(regexpblock_1, profileDir);
+ setExtensionModifiedTime(getFileForAddon(profileDir, regexpblock_1.id), Date.now() + 20000);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s4.userDisabled = true;
+ s5.userDisabled = false;
+});
+
+// Add-ons are initially unblocked then attempts to upgrade to blocked versions
+// in the background which should fail
+add_task(function* run_background_update_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ yield Pbackground_update();
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+});
+
+// Starts with add-ons blocked and then new versions are detected and installed
+// automatically for unblocked versions.
+add_task(function* run_background_update_2_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseShutdownManager();
+
+ getFileForAddon(profileDir, softblock1_1.id).remove(true);
+ getFileForAddon(profileDir, softblock2_1.id).remove(true);
+ getFileForAddon(profileDir, softblock3_1.id).remove(true);
+ getFileForAddon(profileDir, softblock4_1.id).remove(true);
+ getFileForAddon(profileDir, softblock5_1.id).remove(true);
+ getFileForAddon(profileDir, hardblock_1.id).remove(true);
+ getFileForAddon(profileDir, regexpblock_1.id).remove(true);
+
+ startupManager(false);
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_3, profileDir);
+ writeInstallRDFForExtension(softblock2_3, profileDir);
+ writeInstallRDFForExtension(softblock3_3, profileDir);
+ writeInstallRDFForExtension(softblock4_3, profileDir);
+ writeInstallRDFForExtension(softblock5_3, profileDir);
+ writeInstallRDFForExtension(hardblock_3, profileDir);
+ writeInstallRDFForExtension(regexpblock_3, profileDir);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "3.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+
+ yield promiseRestartManager();
+
+ yield Pbackground_update();
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s4.userDisabled = true;
+ s5.userDisabled = true;
+});
+
+// Starts with add-ons blocked and then simulates the user upgrading them to
+// unblocked versions.
+add_task(function* run_manual_update_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseRestartManager();
+ yield Pload_blocklist("manual_update.xml");
+ yield promiseRestartManager();
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+
+ yield promiseRestartManager();
+
+ yield Pmanual_update("2");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s4, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s5, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ // Can't manually update to a hardblocked add-on
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ yield Pmanual_update("3");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s4, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s5, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+});
+
+// Starts with add-ons blocked and then new versions are installed outside of
+// the app to change them to unblocked.
+add_task(function* run_manual_update_2_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseShutdownManager();
+
+ getFileForAddon(profileDir, softblock1_1.id).remove(true);
+ getFileForAddon(profileDir, softblock2_1.id).remove(true);
+ getFileForAddon(profileDir, softblock3_1.id).remove(true);
+ getFileForAddon(profileDir, softblock4_1.id).remove(true);
+ getFileForAddon(profileDir, softblock5_1.id).remove(true);
+ getFileForAddon(profileDir, hardblock_1.id).remove(true);
+ getFileForAddon(profileDir, regexpblock_1.id).remove(true);
+
+ startupManager(false);
+ yield promiseShutdownManager();
+
+ writeInstallRDFForExtension(softblock1_1, profileDir);
+ writeInstallRDFForExtension(softblock2_1, profileDir);
+ writeInstallRDFForExtension(softblock3_1, profileDir);
+ writeInstallRDFForExtension(softblock4_1, profileDir);
+ writeInstallRDFForExtension(softblock5_1, profileDir);
+ writeInstallRDFForExtension(hardblock_1, profileDir);
+ writeInstallRDFForExtension(regexpblock_1, profileDir);
+
+ startupManager(false);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ s2.userDisabled = false;
+ s2.userDisabled = true;
+ check_addon(s2, "1.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ s3.userDisabled = false;
+ check_addon(s3, "1.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ yield promiseRestartManager();
+
+ yield Pmanual_update("2");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "2.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "2.0", true, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "2.0", false, false, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ // Can't manually update to a hardblocked add-on
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+
+ yield promiseRestartManager();
+
+ yield Pmanual_update("3");
+ yield promiseRestartManager();
+
+ [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s2, "3.0", true, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(s3, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(h, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ check_addon(r, "3.0", false, false, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ s1.userDisabled = false;
+ s2.userDisabled = false;
+ s4.userDisabled = true;
+});
+
+// Uses the API to install blocked add-ons from the local filesystem
+add_task(function* run_local_install_test() {
+ do_print("Test: " + arguments.callee.name + "\n");
+ yield promiseShutdownManager();
+
+ getFileForAddon(profileDir, softblock1_1.id).remove(true);
+ getFileForAddon(profileDir, softblock2_1.id).remove(true);
+ getFileForAddon(profileDir, softblock3_1.id).remove(true);
+ getFileForAddon(profileDir, softblock4_1.id).remove(true);
+ getFileForAddon(profileDir, softblock5_1.id).remove(true);
+ getFileForAddon(profileDir, hardblock_1.id).remove(true);
+ getFileForAddon(profileDir, regexpblock_1.id).remove(true);
+
+ startupManager(false);
+
+ yield promiseInstallAllFiles([
+ do_get_file("addons/blocklist_soft1_1.xpi"),
+ do_get_file("addons/blocklist_soft2_1.xpi"),
+ do_get_file("addons/blocklist_soft3_1.xpi"),
+ do_get_file("addons/blocklist_soft4_1.xpi"),
+ do_get_file("addons/blocklist_soft5_1.xpi"),
+ do_get_file("addons/blocklist_hard1_1.xpi"),
+ do_get_file("addons/blocklist_regexp1_1.xpi")
+ ]);
+
+ let aInstalls = yield new Promise((resolve, reject) => {
+ AddonManager.getAllInstalls(resolve)
+ });
+ // Should have finished all installs without needing to restart
+ do_check_eq(aInstalls.length, 0);
+
+ let [s1, s2, s3, s4, s5, h, r] = yield promiseAddonsByIDs(ADDON_IDS);
+
+ check_addon(s1, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s2, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(s3, "1.0", true, true, Ci.nsIBlocklistService.STATE_SOFTBLOCKED);
+ check_addon(h, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+ check_addon(r, "1.0", false, false, Ci.nsIBlocklistService.STATE_BLOCKED);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
new file mode 100644
index 000000000..ff58599bc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap.js
@@ -0,0 +1,1403 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const APP_STARTUP = 1;
+const APP_SHUTDOWN = 2;
+const ADDON_ENABLE = 3;
+const ADDON_DISABLE = 4;
+const ADDON_INSTALL = 5;
+const ADDON_UNINSTALL = 6;
+const ADDON_UPGRADE = 7;
+const ADDON_DOWNGRADE = 8;
+
+const ID1 = "bootstrap1@tests.mozilla.org";
+const ID2 = "bootstrap2@tests.mozilla.org";
+
+// This verifies that bootstrappable add-ons can be used without restarts.
+Components.utils.import("resource://gre/modules/Services.jsm");
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+// Enable loading extensions from the user scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+
+BootstrapMonitor.init();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const userExtDir = gProfD.clone();
+userExtDir.append("extensions2");
+userExtDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userExtDir.parent);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(undefined);
+gPort = testserver.identity.primaryPort;
+
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+function getStartupReason() {
+ let info = BootstrapMonitor.started.get(ID1);
+ return info ? info.reason : undefined;
+}
+
+function getShutdownReason() {
+ let info = BootstrapMonitor.stopped.get(ID1);
+ return info ? info.reason : undefined;
+}
+
+function getInstallReason() {
+ let info = BootstrapMonitor.installed.get(ID1);
+ return info ? info.reason : undefined;
+}
+
+function getUninstallReason() {
+ let info = BootstrapMonitor.uninstalled.get(ID1);
+ return info ? info.reason : undefined;
+}
+
+function getStartupOldVersion() {
+ let info = BootstrapMonitor.started.get(ID1);
+ return info ? info.data.oldVersion : undefined;
+}
+
+function getShutdownNewVersion() {
+ let info = BootstrapMonitor.stopped.get(ID1);
+ return info ? info.data.newVersion : undefined;
+}
+
+function getInstallOldVersion() {
+ let info = BootstrapMonitor.installed.get(ID1);
+ return info ? info.data.oldVersion : undefined;
+}
+
+function getUninstallNewVersion() {
+ let info = BootstrapMonitor.uninstalled.get(ID1);
+ return info ? info.data.newVersion : undefined;
+}
+
+function do_check_bootstrappedPref(aCallback) {
+ let data = Services.prefs.getCharPref("extensions.bootstrappedAddons");
+ data = JSON.parse(data);
+
+ AddonManager.getAddonsByTypes(["extension"], function(aAddons) {
+ for (let addon of aAddons) {
+ if (!addon.id.endsWith("@tests.mozilla.org"))
+ continue;
+ if (!addon.isActive)
+ continue;
+ if (addon.operationsRequiringRestart != AddonManager.OP_NEEDS_RESTART_NONE)
+ continue;
+
+ do_check_true(addon.id in data);
+ let addonData = data[addon.id];
+ delete data[addon.id];
+
+ do_check_eq(addonData.version, addon.version);
+ do_check_eq(addonData.type, addon.type);
+ let file = addon.getResourceURI().QueryInterface(Components.interfaces.nsIFileURL).file;
+ do_check_eq(addonData.descriptor, file.persistentDescriptor);
+ }
+ do_check_eq(Object.keys(data).length, 0);
+
+ do_execute_soon(aCallback);
+ });
+}
+
+
+function run_test() {
+ do_test_pending();
+
+ startupManager();
+
+ do_check_false(gExtensionsJSON.exists());
+
+ do_check_false(gExtensionsINI.exists());
+
+ run_test_1();
+}
+
+// Tests that installing doesn't require a restart
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_neq(install.addon.syncGUID, null);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_true(install.addon.hasResource("bootstrap.js"));
+ do_check_false(install.addon.hasResource("foo.bar"));
+ do_check_eq(install.addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL, 0);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ let addon = install.addon;
+
+ BootstrapMonitor.promiseAddonStartup(ID1).then(function() {
+ do_check_bootstrappedPref(function() {
+ check_test_1(addon.syncGUID);
+ });
+ });
+
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_check_true(addon.hasResource("install.rdf"));
+
+ // startup should not have been called yet.
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ });
+ install.install();
+ });
+}
+
+function check_test_1(installSyncGUID) {
+ do_check_false(gExtensionsINI.exists());
+
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_neq(b1.syncGUID, null);
+ do_check_eq(b1.syncGUID, installSyncGUID);
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_true(b1.hasResource("bootstrap.js"));
+ do_check_false(b1.hasResource("foo.bar"));
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ let dir = do_get_addon_root_uri(profileDir, ID1);
+ do_check_eq(b1.getResourceURI("bootstrap.js").spec, dir + "bootstrap.js");
+
+ AddonManager.getAddonsWithOperationsByTypes(null, function(list) {
+ do_check_eq(list.length, 0);
+
+ do_execute_soon(run_test_2);
+ });
+ });
+ });
+}
+
+// Tests that disabling doesn't require a restart
+function run_test_2() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ prepare_test({
+ [ID1]: [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_DISABLE, 0);
+ b1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), ADDON_DISABLE);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_true(newb1.userDisabled);
+ do_check_false(newb1.isActive);
+
+ do_check_bootstrappedPref(run_test_3);
+ });
+ });
+}
+
+// Test that restarting doesn't accidentally re-enable
+function run_test_3() {
+ shutdownManager();
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), ADDON_DISABLE);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ startupManager(false);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), ADDON_DISABLE);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ do_check_false(gExtensionsINI.exists());
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+
+ do_check_bootstrappedPref(run_test_4);
+ });
+}
+
+// Tests that enabling doesn't require a restart
+function run_test_4() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ prepare_test({
+ [ID1]: [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_ENABLE, 0);
+ b1.userDisabled = false;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_ENABLE);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_false(newb1.userDisabled);
+ do_check_true(newb1.isActive);
+
+ do_check_bootstrappedPref(run_test_5);
+ });
+ });
+}
+
+// Tests that a restart shuts down and restarts the add-on
+function run_test_5() {
+ shutdownManager();
+ // By the time we've shut down, the database must have been written
+ do_check_true(gExtensionsJSON.exists());
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), APP_SHUTDOWN);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+ startupManager(false);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), APP_STARTUP);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ do_check_false(isExtensionInAddonsList(profileDir, b1.id));
+
+ do_check_bootstrappedPref(run_test_6);
+ });
+}
+
+// Tests that installing an upgrade doesn't require a restart
+function run_test_6() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+
+ BootstrapMonitor.promiseAddonStartup(ID1).then(check_test_6);
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ });
+ install.install();
+ });
+}
+
+function check_test_6() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "2.0");
+ do_check_eq(getStartupReason(), ADDON_UPGRADE);
+ do_check_eq(getInstallOldVersion(), 1);
+ do_check_eq(getStartupOldVersion(), 1);
+ do_check_eq(getShutdownReason(), ADDON_UPGRADE);
+ do_check_eq(getShutdownNewVersion(), 2);
+ do_check_eq(getUninstallNewVersion(), 2);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+ do_check_in_crash_annotation(ID1, "2.0");
+
+ do_check_bootstrappedPref(run_test_7);
+ });
+}
+
+// Tests that uninstalling doesn't require a restart
+function run_test_7() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ prepare_test({
+ [ID1]: [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+ b1.uninstall();
+
+ do_check_bootstrappedPref(check_test_7);
+ });
+}
+
+function check_test_7() {
+ ensure_test_completed();
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), ADDON_UNINSTALL);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_not_in_crash_annotation(ID1, "2.0");
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ do_check_eq(b1, null);
+
+ restartManager();
+
+ AddonManager.getAddonByID(ID1, function(newb1) {
+ do_check_eq(newb1, null);
+
+ do_check_bootstrappedPref(run_test_8);
+ });
+ }));
+}
+
+// Test that a bootstrapped extension dropped into the profile loads properly
+// on startup and doesn't cause an EM restart
+function run_test_8() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+ ID1);
+
+ startupManager(false);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ do_check_bootstrappedPref(run_test_9);
+ });
+}
+
+// Test that items detected as removed during startup get removed properly
+function run_test_9() {
+ shutdownManager();
+
+ manuallyUninstall(profileDir, ID1);
+ BootstrapMonitor.clear(ID1);
+
+ startupManager(false);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_eq(b1, null);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ do_check_bootstrappedPref(run_test_10);
+ });
+}
+
+
+// Tests that installing a downgrade sends the right reason
+function run_test_10() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_true(install.addon.hasResource("bootstrap.js"));
+ do_check_false(install.addon.hasResource("foo.bar"));
+ do_check_not_in_crash_annotation(ID1, "2.0");
+
+ BootstrapMonitor.promiseAddonStartup(ID1).then(check_test_10_pt1);
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_print("Waiting for startup of bootstrap1_2");
+ });
+ install.install();
+ });
+}
+
+function check_test_10_pt1() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "2.0");
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_true(b1.hasResource("bootstrap.js"));
+ do_check_false(b1.hasResource("foo.bar"));
+ do_check_in_crash_annotation(ID1, "2.0");
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+
+ BootstrapMonitor.promiseAddonStartup(ID1).then(check_test_10_pt2);
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() { });
+ install.install();
+ });
+ });
+}
+
+function check_test_10_pt2() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_DOWNGRADE);
+ do_check_eq(getInstallOldVersion(), 2);
+ do_check_eq(getStartupOldVersion(), 2);
+ do_check_eq(getShutdownReason(), ADDON_DOWNGRADE);
+ do_check_eq(getShutdownNewVersion(), 1);
+ do_check_eq(getUninstallNewVersion(), 1);
+ do_check_in_crash_annotation(ID1, "1.0");
+ do_check_not_in_crash_annotation(ID1, "2.0");
+
+ do_check_bootstrappedPref(run_test_11);
+ });
+}
+
+// Tests that uninstalling a disabled add-on still calls the uninstall method
+function run_test_11() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ prepare_test({
+ [ID1]: [
+ ["onDisabling", false],
+ "onDisabled",
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ b1.userDisabled = true;
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_eq(getShutdownReason(), ADDON_DISABLE);
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ b1.uninstall();
+
+ check_test_11();
+ });
+}
+
+function check_test_11() {
+ ensure_test_completed();
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ do_check_bootstrappedPref(run_test_12);
+}
+
+// Tests that bootstrapped extensions are correctly loaded even if the app is
+// upgraded at the same time
+function run_test_12() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+ ID1);
+
+ startupManager(true);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ b1.uninstall();
+ do_execute_soon(test_12_restart);
+ });
+}
+
+function test_12_restart() {
+ restartManager();
+ do_check_bootstrappedPref(run_test_13);
+}
+
+
+// Tests that installing a bootstrapped extension with an invalid application
+// entry doesn't call it's startup method
+function run_test_13() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_3"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_not_in_crash_annotation(ID1, "3.0");
+
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_13));
+ install.install();
+ });
+}
+
+function check_test_13() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "3.0");
+ do_check_true(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons
+ BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though
+ do_check_not_in_crash_annotation(ID1, "3.0");
+
+ do_execute_soon(test_13_restart);
+ });
+ });
+}
+
+function test_13_restart() {
+ restartManager();
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "3.0");
+ do_check_true(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons
+ BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though
+ do_check_not_in_crash_annotation(ID1, "3.0");
+
+ do_check_bootstrappedPref(function() {
+ b1.uninstall();
+ do_execute_soon(run_test_14);
+ });
+ });
+}
+
+// Tests that a bootstrapped extension with an invalid target application entry
+// does not get loaded when detected during startup
+function run_test_14() {
+ restartManager();
+
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_3"), profileDir,
+ ID1);
+
+ startupManager(false);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "3.0");
+ do_check_true(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "3.0"); // We call install even for disabled add-ons
+ BootstrapMonitor.checkAddonNotStarted(ID1); // Should not have called startup though
+ do_check_not_in_crash_annotation(ID1, "3.0");
+
+ do_check_bootstrappedPref(function() {
+ b1.uninstall();
+
+ run_test_15();
+ });
+ });
+}
+
+// Tests that upgrading a disabled bootstrapped extension still calls uninstall
+// and install but doesn't startup the new version
+function run_test_15() {
+ BootstrapMonitor.promiseAddonStartup(ID1).then(function test_15_after_startup() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ b1.userDisabled = true;
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_true(install.addon.userDisabled);
+
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_15));
+ install.install();
+ });
+ });
+ });
+ installAllFiles([do_get_addon("test_bootstrap1_1")], function test_15_addon_installed() { });
+}
+
+function check_test_15() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ do_check_bootstrappedPref(function() {
+ restartManager();
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "2.0");
+ do_check_false(b1_2.appDisabled);
+ do_check_true(b1_2.userDisabled);
+ do_check_false(b1_2.isActive);
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ b1_2.uninstall();
+
+ run_test_16();
+ }));
+ });
+ });
+}
+
+// Tests that bootstrapped extensions don't get loaded when in safe mode
+function run_test_16() {
+ BootstrapMonitor.promiseAddonStartup(ID1).then(function test_16_after_startup() {
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ do_check_eq(b1.iconURL, "chrome://foo/skin/icon.png");
+ do_check_eq(b1.aboutURL, "chrome://foo/content/about.xul");
+ do_check_eq(b1.optionsURL, "chrome://foo/content/options.xul");
+
+ shutdownManager();
+
+ // Should have stopped
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ gAppInfo.inSafeMode = true;
+ startupManager(false);
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1_2) {
+ // Should still be stopped
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ do_check_false(b1_2.isActive);
+ do_check_eq(b1_2.iconURL, null);
+ do_check_eq(b1_2.aboutURL, null);
+ do_check_eq(b1_2.optionsURL, null);
+
+ shutdownManager();
+ gAppInfo.inSafeMode = false;
+ startupManager(false);
+
+ // Should have started
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, function(b1_3) {
+ b1_3.uninstall();
+
+ do_execute_soon(run_test_17);
+ });
+ }));
+ }));
+ });
+ installAllFiles([do_get_addon("test_bootstrap1_1")], function() { });
+}
+
+// Check that a bootstrapped extension in a non-profile location is loaded
+function run_test_17() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_1"), userExtDir,
+ ID1);
+
+ startupManager();
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ do_check_bootstrappedPref(run_test_18);
+ });
+}
+
+// Check that installing a new bootstrapped extension in the profile replaces
+// the existing one
+function run_test_18() {
+ BootstrapMonitor.promiseAddonStartup(ID1).then(function test_18_after_startup() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "2.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ do_check_eq(getShutdownReason(), ADDON_UPGRADE);
+ do_check_eq(getUninstallReason(), ADDON_UPGRADE);
+ do_check_eq(getInstallReason(), ADDON_UPGRADE);
+ do_check_eq(getStartupReason(), ADDON_UPGRADE);
+
+ do_check_eq(getShutdownNewVersion(), 2);
+ do_check_eq(getUninstallNewVersion(), 2);
+ do_check_eq(getInstallOldVersion(), 1);
+ do_check_eq(getStartupOldVersion(), 1);
+
+ do_check_bootstrappedPref(run_test_19);
+ });
+ });
+ installAllFiles([do_get_addon("test_bootstrap1_2")], function() { });
+}
+
+// Check that uninstalling the profile version reveals the non-profile one
+function run_test_19() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // The revealed add-on gets activated asynchronously
+ prepare_test({
+ [ID1]: [
+ ["onUninstalling", false],
+ "onUninstalled",
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [], check_test_19);
+
+ b1.uninstall();
+ });
+}
+
+function check_test_19() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // Should have reverted to the older version
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ // TODO these reasons really should be ADDON_DOWNGRADE (bug 607818)
+ do_check_eq(getShutdownReason(), ADDON_UNINSTALL);
+ do_check_eq(getUninstallReason(), ADDON_UNINSTALL);
+ do_check_eq(getInstallReason(), ADDON_INSTALL);
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_eq(getUninstallNewVersion(), undefined);
+ do_check_eq(getInstallOldVersion(), undefined);
+ do_check_eq(getStartupOldVersion(), undefined);
+
+ do_check_bootstrappedPref(run_test_20);
+ });
+}
+
+// Check that a new profile extension detected at startup replaces the non-profile
+// one
+function run_test_20() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir,
+ ID1);
+
+ startupManager();
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "2.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ do_check_eq(getShutdownReason(), APP_SHUTDOWN);
+ do_check_eq(getUninstallReason(), ADDON_UPGRADE);
+ do_check_eq(getInstallReason(), ADDON_UPGRADE);
+ do_check_eq(getStartupReason(), APP_STARTUP);
+
+ do_check_eq(getShutdownNewVersion(), undefined);
+ do_check_eq(getUninstallNewVersion(), 2);
+ do_check_eq(getInstallOldVersion(), 1);
+ do_check_eq(getStartupOldVersion(), undefined);
+
+ do_execute_soon(run_test_21);
+ });
+}
+
+// Check that a detected removal reveals the non-profile one
+function run_test_21() {
+ shutdownManager();
+
+ do_check_eq(getShutdownReason(), APP_SHUTDOWN);
+ do_check_eq(getShutdownNewVersion(), undefined);
+
+ manuallyUninstall(profileDir, ID1);
+ BootstrapMonitor.clear(ID1);
+
+ startupManager();
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ // This won't be set as the bootstrap script was gone so we couldn't
+ // uninstall it properly
+ do_check_eq(getUninstallReason(), undefined);
+ do_check_eq(getUninstallNewVersion(), undefined);
+
+ do_check_eq(getInstallReason(), ADDON_DOWNGRADE);
+ do_check_eq(getInstallOldVersion(), 2);
+
+ do_check_eq(getStartupReason(), APP_STARTUP);
+ do_check_eq(getStartupOldVersion(), undefined);
+
+ do_check_bootstrappedPref(function() {
+ shutdownManager();
+
+ manuallyUninstall(userExtDir, ID1);
+ BootstrapMonitor.clear(ID1);
+
+ startupManager(false);
+ run_test_22();
+ });
+ });
+}
+
+// Check that an upgrade from the filesystem is detected and applied correctly
+function run_test_22() {
+ shutdownManager();
+
+ let file = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir,
+ ID1);
+
+ // Make it look old so changes are detected
+ setExtensionModifiedTime(file, file.lastModifiedTime - 5000);
+
+ startupManager();
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+
+ shutdownManager();
+
+ do_check_eq(getShutdownReason(), APP_SHUTDOWN);
+ do_check_eq(getShutdownNewVersion(), undefined);
+
+ manuallyUninstall(profileDir, ID1);
+ BootstrapMonitor.clear(ID1);
+ manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir,
+ ID1);
+
+ startupManager();
+
+ AddonManager.getAddonByID(ID1, function(b1_2) {
+ // Should have installed and started
+ BootstrapMonitor.checkAddonInstalled(ID1, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "2.0");
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "2.0");
+ do_check_true(b1_2.isActive);
+ do_check_false(b1_2.isSystem);
+
+ // This won't be set as the bootstrap script was gone so we couldn't
+ // uninstall it properly
+ do_check_eq(getUninstallReason(), undefined);
+ do_check_eq(getUninstallNewVersion(), undefined);
+
+ do_check_eq(getInstallReason(), ADDON_UPGRADE);
+ do_check_eq(getInstallOldVersion(), 1);
+ do_check_eq(getStartupReason(), APP_STARTUP);
+ do_check_eq(getStartupOldVersion(), undefined);
+
+ do_check_bootstrappedPref(function() {
+ b1_2.uninstall();
+
+ run_test_23();
+ });
+ });
+ }));
+}
+
+
+// Tests that installing from a URL doesn't require a restart
+function run_test_23() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_bootstrap1_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], function() {
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Bootstrap 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_true(install.addon.hasResource("bootstrap.js"));
+ do_check_false(install.addon.hasResource("foo.bar"));
+ do_check_eq(install.addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL, 0);
+ do_check_not_in_crash_annotation(ID1, "1.0");
+
+ let addon = install.addon;
+ prepare_test({
+ [ID1]: [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_check_true(addon.hasResource("install.rdf"));
+ do_check_bootstrappedPref(check_test_23);
+ });
+ });
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_23() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_execute_soon(function test_23_after_startup() {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ do_check_eq(getStartupReason(), ADDON_INSTALL);
+ do_check_eq(getStartupOldVersion(), undefined);
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_true(b1.hasResource("bootstrap.js"));
+ do_check_false(b1.hasResource("foo.bar"));
+ do_check_in_crash_annotation(ID1, "1.0");
+
+ let dir = do_get_addon_root_uri(profileDir, ID1);
+ do_check_eq(b1.getResourceURI("bootstrap.js").spec, dir + "bootstrap.js");
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(list) {
+ do_check_eq(list.length, 0);
+
+ restartManager();
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1_2) {
+ b1_2.uninstall();
+ restartManager();
+
+ testserver.stop(run_test_24);
+ }));
+ }));
+ });
+ });
+ });
+}
+
+// Tests that we recover from a broken preference
+function run_test_24() {
+ do_print("starting 24");
+
+ Promise.all([BootstrapMonitor.promiseAddonStartup(ID2),
+ promiseInstallAllFiles([do_get_addon("test_bootstrap1_1"), do_get_addon("test_bootstrap2_1")])])
+ .then(function test_24_pref() {
+ do_print("test 24 got prefs");
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID2, "1.0");
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID2, "1.0");
+
+ shutdownManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+ BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID2);
+
+ // Break the preference
+ let bootstrappedAddons = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
+ bootstrappedAddons[ID1].descriptor += "foo";
+ Services.prefs.setCharPref("extensions.bootstrappedAddons", JSON.stringify(bootstrappedAddons));
+
+ startupManager(false);
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+ BootstrapMonitor.checkAddonInstalled(ID2, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID2, "1.0");
+
+ run_test_25();
+ });
+}
+
+// Tests that updating from a bootstrappable add-on to a normal add-on calls
+// the uninstall method
+function run_test_25() {
+ BootstrapMonitor.promiseAddonStartup(ID1).then(function test_25_after_pref() {
+ do_print("test 25 pref change detected");
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ installAllFiles([do_get_addon("test_bootstrap1_4")], function() {
+ // Needs a restart to complete this so the old version stays running
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ do_check_true(hasFlag(b1.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ do_check_eq(getUninstallReason(), ADDON_UPGRADE);
+ do_check_eq(getUninstallNewVersion(), 4);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "4.0");
+ do_check_true(b1_2.isActive);
+ do_check_false(b1_2.isSystem);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_bootstrappedPref(run_test_26);
+ });
+ }));
+ });
+ });
+ installAllFiles([do_get_addon("test_bootstrap1_1")], function test_25_installed() {
+ do_print("test 25 install done");
+ });
+}
+
+// Tests that updating from a normal add-on to a bootstrappable add-on calls
+// the install method
+function run_test_26() {
+ installAllFiles([do_get_addon("test_bootstrap1_1")], function() {
+ // Needs a restart to complete this
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "4.0");
+ do_check_true(b1.isActive);
+ do_check_false(b1.isSystem);
+ do_check_true(hasFlag(b1.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ do_check_eq(getInstallReason(), ADDON_DOWNGRADE);
+ do_check_eq(getInstallOldVersion(), 4);
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ AddonManager.getAddonByID(ID1, function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "1.0");
+ do_check_true(b1_2.isActive);
+ do_check_false(b1_2.isSystem);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_bootstrappedPref(run_test_27);
+ });
+ }));
+ });
+}
+
+// Tests that updating from a bootstrappable add-on to a normal add-on while
+// disabled calls the uninstall method
+function run_test_27() {
+ AddonManager.getAddonByID(ID1, function(b1) {
+ do_check_neq(b1, null);
+ b1.userDisabled = true;
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.isActive);
+ do_check_eq(b1.pendingOperations, AddonManager.PENDING_NONE);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ installAllFiles([do_get_addon("test_bootstrap1_4")], function() {
+ // Updating disabled things happens immediately
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ do_check_eq(getUninstallReason(), ADDON_UPGRADE);
+ do_check_eq(getUninstallNewVersion(), 4);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "4.0");
+ do_check_false(b1_2.isActive);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID1);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, function(b1_3) {
+ do_check_neq(b1_3, null);
+ do_check_eq(b1_3.version, "4.0");
+ do_check_false(b1_3.isActive);
+ do_check_eq(b1_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_bootstrappedPref(run_test_28);
+ });
+ }));
+ });
+ });
+}
+
+// Tests that updating from a normal add-on to a bootstrappable add-on when
+// disabled calls the install method but not the startup method
+function run_test_28() {
+ installAllFiles([do_get_addon("test_bootstrap1_1")], function() {
+ do_execute_soon(function bootstrap_disabled_downgrade_check() {
+ // Doesn't need a restart to complete this
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ do_check_eq(getInstallReason(), ADDON_DOWNGRADE);
+ do_check_eq(getInstallOldVersion(), 4);
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.isActive);
+ do_check_true(b1.userDisabled);
+ do_check_eq(b1.pendingOperations, AddonManager.PENDING_NONE);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID1);
+
+ AddonManager.getAddonByID(ID1, function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_true(b1_2.userDisabled);
+ b1_2.userDisabled = false;
+ do_check_eq(b1_2.version, "1.0");
+ do_check_true(b1_2.isActive);
+ do_check_false(b1_2.isSystem);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+ BootstrapMonitor.checkAddonInstalled(ID1, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID1, "1.0");
+
+ do_check_bootstrappedPref(do_test_finished);
+ });
+ }));
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js
new file mode 100644
index 000000000..101d49510
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_const.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+startupManager();
+
+add_task(function*() {
+ let sawInstall = false;
+ Services.obs.addObserver(function() {
+ sawInstall = true;
+ }, "addon-install", false);
+
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap_const")]);
+
+ ok(sawInstall);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js
new file mode 100644
index 000000000..29b538d21
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_globals.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that bootstrap.js has the expected globals defined
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+const EXPECTED_GLOBALS = [
+ ["Worker", "function"],
+ ["ChromeWorker", "function"],
+ ["console", "object"]
+];
+
+function run_test() {
+ do_test_pending();
+ startupManager();
+ let sawGlobals = false;
+
+ Services.obs.addObserver(function(subject) {
+ subject.wrappedJSObject.expectedGlobals = EXPECTED_GLOBALS;
+ }, "bootstrap-request-globals", false);
+
+ Services.obs.addObserver(function({ wrappedJSObject: seenGlobals }) {
+ for (let [name, ] of EXPECTED_GLOBALS)
+ do_check_true(seenGlobals.has(name));
+
+ sawGlobals = true;
+ }, "bootstrap-seen-globals", false);
+
+ installAllFiles([do_get_addon("bootstrap_globals")], function() {
+ do_check_true(sawGlobals);
+ shutdownManager();
+ do_test_finished();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_resource.js b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_resource.js
new file mode 100644
index 000000000..7b7883225
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bootstrap_resource.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that resource protocol substitutions are set and unset for bootstrapped add-ons.
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ let resourceProtocol = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Components.interfaces.nsIResProtocolHandler);
+ startupManager();
+
+ installAllFiles([do_get_addon("test_chromemanifest_6")],
+ function() {
+
+ AddonManager.getAddonByID("addon6@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ do_check_true(addon.isActive);
+ do_check_true(resourceProtocol.hasSubstitution("test-addon-1"));
+
+ prepare_test({
+ "addon6@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ do_check_eq(addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_DISABLE, 0);
+ addon.userDisabled = true;
+ ensure_test_completed();
+ do_check_false(resourceProtocol.hasSubstitution("test-addon-1"))
+
+ prepare_test({
+ "addon6@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ do_check_eq(addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_ENABLE, 0);
+ addon.userDisabled = false;
+ ensure_test_completed();
+ do_check_true(resourceProtocol.hasSubstitution("test-addon-1"));
+
+ do_execute_soon(do_test_finished);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901.js
new file mode 100644
index 000000000..c13531dff
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+ run_next_test();
+}
+
+add_task(function* () {
+ let profileDir = OS.Constants.Path.profileDir;
+ let trashDir = OS.Path.join(profileDir, "extensions", "trash");
+ let testFile = OS.Path.join(trashDir, "test.txt");
+
+ yield OS.File.makeDir(trashDir, {
+ from: profileDir,
+ ignoreExisting: true
+ });
+
+ let trashDirExists = yield OS.File.exists(trashDir);
+ ok(trashDirExists, "trash directory should have been created");
+
+ let file = yield OS.File.open(testFile, {create: true}, {winShare: 0});
+ let fileExists = yield OS.File.exists(testFile);
+ ok(fileExists, "test.txt should have been created in " + trashDir);
+
+ yield promiseInstallAllFiles([do_get_addon("test_install1")]);
+ yield promiseRestartManager();
+ fileExists = yield OS.File.exists(testFile);
+ ok(fileExists, "test.txt still exists");
+ yield file.close();
+ yield OS.File.removeDir(OS.Path.join(OS.Constants.Path.profileDir, "extensions"));
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901_2.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901_2.js
new file mode 100644
index 000000000..8e9f30ef2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug1180901_2.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+ run_next_test();
+}
+
+add_task(function* () {
+ let profileDir = OS.Constants.Path.profileDir;
+ let trashDir = OS.Path.join(profileDir, "extensions", "trash");
+ let testFile = OS.Path.join(trashDir, "test.txt");
+
+ yield OS.File.makeDir(trashDir, {
+ from: profileDir,
+ ignoreExisting: true
+ });
+
+ let trashDirExists = yield OS.File.exists(trashDir);
+ ok(trashDirExists, "trash directory should have been created");
+
+ let file = yield OS.File.open(testFile, {create: true}, {winShare: 0});
+ let fileExists = yield OS.File.exists(testFile);
+ ok(fileExists, "test.txt should have been created in " + trashDir);
+
+ let promiseInstallStatus = new Promise((resolve, reject) => {
+ let listener = {
+ onInstallFailed: function() {
+ AddonManager.removeInstallListener(listener);
+ reject("extension installation should not have failed");
+ },
+ onInstallEnded: function() {
+ AddonManager.removeInstallListener(listener);
+ ok(true, "extension installation should not have failed");
+ resolve();
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+ });
+
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")]);
+
+ // The testFile should still exist at this point because we have not
+ // yet closed the file handle and as a result, Windows cannot remove it.
+ fileExists = yield OS.File.exists(testFile);
+ ok(fileExists, "test.txt should still exist");
+
+ // Wait for the AddonManager to tell us if the installation of the extension
+ // succeeded or not.
+ yield promiseInstallStatus;
+
+ // Cleanup
+ yield promiseShutdownManager();
+ yield file.close();
+ yield OS.File.remove(testFile);
+ yield OS.File.removeDir(trashDir);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug299716.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug299716.js
new file mode 100644
index 000000000..66656abe6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug299716.js
@@ -0,0 +1,208 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+// Update check listener.
+const checkListener = {
+ pendingCount: 0,
+
+ onUpdateAvailable: function onUpdateAvailable(aAddon, aInstall) {
+ for (let currentAddon of ADDONS) {
+ if (currentAddon.id == aAddon.id) {
+ currentAddon.newInstall = aInstall;
+ return;
+ }
+ }
+ },
+
+ onUpdateFinished: function onUpdateFinished() {
+ if (--this.pendingCount == 0)
+ next_test();
+ }
+}
+
+// Get the HTTP server.
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver;
+
+var ADDONS = [
+ // XPCShell
+ {
+ id: "bug299716-a@tests.mozilla.org",
+ addon: "test_bug299716_a_1",
+ installed: true,
+ item: null,
+ newInstall: null
+ },
+
+ // Toolkit
+ {
+ id: "bug299716-b@tests.mozilla.org",
+ addon: "test_bug299716_b_1",
+ installed: true,
+ item: null,
+ newInstall: null
+ },
+
+ // XPCShell + Toolkit
+ {
+ id: "bug299716-c@tests.mozilla.org",
+ addon: "test_bug299716_c_1",
+ installed: true,
+ item: null,
+ newInstall: null
+ },
+
+ // XPCShell (Toolkit invalid)
+ {
+ id: "bug299716-d@tests.mozilla.org",
+ addon: "test_bug299716_d_1",
+ installed: true,
+ item: null,
+ newInstall: null
+ },
+
+ // Toolkit (XPCShell invalid)
+ {
+ id: "bug299716-e@tests.mozilla.org",
+ addon: "test_bug299716_e_1",
+ installed: false,
+ item: null,
+ newInstall: null,
+ failedAppName: "XPCShell"
+ },
+
+ // None (XPCShell, Toolkit invalid)
+ {
+ id: "bug299716-f@tests.mozilla.org",
+ addon: "test_bug299716_f_1",
+ installed: false,
+ item: null,
+ newInstall: null,
+ failedAppName: "XPCShell"
+ },
+
+ // None (Toolkit invalid)
+ {
+ id: "bug299716-g@tests.mozilla.org",
+ addon: "test_bug299716_g_1",
+ installed: false,
+ item: null,
+ newInstall: null,
+ failedAppName: "Toolkit"
+ },
+];
+
+var next_test = function() {};
+
+function do_check_item(aItem, aVersion, aAddonsEntry) {
+ if (aAddonsEntry.installed) {
+ if (aItem == null)
+ do_throw("Addon " + aAddonsEntry.id + " wasn't detected");
+ if (aItem.version != aVersion)
+ do_throw("Addon " + aAddonsEntry.id + " was version " + aItem.version + " instead of " + aVersion);
+ } else if (aItem != null) {
+ do_throw("Addon " + aAddonsEntry.id + " was detected");
+ }
+}
+
+/**
+ * Start the test by installing extensions.
+ */
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "5", "1.9");
+
+ const dataDir = do_get_file("data");
+ const addonsDir = do_get_addon(ADDONS[0].addon).parent;
+
+ // Make sure we can actually get our data files.
+ const xpiFile = addonsDir.clone();
+ xpiFile.append("test_bug299716_a_2.xpi");
+ do_check_true(xpiFile.exists());
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/addons/", addonsDir);
+ testserver.registerDirectory("/data/", dataDir);
+ testserver.start(4444);
+
+ // Make sure we can fetch the files over HTTP.
+ const Ci = Components.interfaces;
+ const xhr = Components.classes["@mozilla.org/xmlextras/xmlhttprequest;1"]
+ .createInstance(Ci.nsIXMLHttpRequest)
+ xhr.open("GET", "http://localhost:4444/addons/test_bug299716_a_2.xpi", false);
+ xhr.send(null);
+ do_check_true(xhr.status == 200);
+
+ xhr.open("GET", "http://localhost:4444/data/test_bug299716.rdf", false);
+ xhr.send(null);
+ do_check_true(xhr.status == 200);
+
+ // Start the real test.
+ startupManager();
+ dump("\n\n*** INSTALLING NEW ITEMS\n\n");
+
+ installAllFiles(ADDONS.map(a => do_get_addon(a.addon)), run_test_pt2,
+ true);
+}
+
+/**
+ * Check the versions of all items, and ask the extension manager to find updates.
+ */
+function run_test_pt2() {
+ dump("\n\n*** DONE INSTALLING NEW ITEMS\n\n");
+ dump("\n\n*** RESTARTING EXTENSION MANAGER\n\n");
+ restartManager();
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(items) {
+ dump("\n\n*** REQUESTING UPDATE\n\n");
+ // checkListener will call run_test_pt3().
+ next_test = run_test_pt3;
+
+ // Try to update the items.
+ for (var i = 0; i < ADDONS.length; i++) {
+ var item = items[i];
+ do_check_item(item, "0.1", ADDONS[i]);
+
+ if (item) {
+ checkListener.pendingCount++;
+ ADDONS[i].item = item;
+ item.findUpdates(checkListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ }
+ }
+ });
+}
+
+/**
+ * Install new items for each enabled extension.
+ */
+function run_test_pt3() {
+ // Install the new items.
+ dump("\n\n*** UPDATING ITEMS\n\n");
+ completeAllInstalls(ADDONS.filter(a => a.newInstall).map(a => a.newInstall),
+ run_test_pt4);
+}
+
+/**
+ * Check the final version of each extension.
+ */
+function run_test_pt4() {
+ dump("\n\n*** RESTARTING EXTENSION MANAGER\n\n");
+ restartManager();
+
+ dump("\n\n*** FINAL CHECKS\n\n");
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(items) {
+ for (var i = 0; i < ADDONS.length; i++) {
+ var item = items[i];
+ do_check_item(item, "0.2", ADDONS[i]);
+ }
+
+ testserver.stop(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug299716_2.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug299716_2.js
new file mode 100644
index 000000000..c183edad4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug299716_2.js
@@ -0,0 +1,50 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+// Get the HTTP server.
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver;
+
+var ADDON = {
+ id: "bug299716-2@tests.mozilla.org",
+ addon: "test_bug299716_2"
+};
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9");
+
+ const dataDir = do_get_file("data");
+ const addonsDir = do_get_addon(ADDON.addon).parent;
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/addons/", addonsDir);
+ testserver.registerDirectory("/data/", dataDir);
+ testserver.start(4444);
+
+ startupManager();
+
+ installAllFiles([do_get_addon(ADDON.addon)], function() {
+ restartManager();
+
+ AddonManager.getAddonByID(ADDON.id, function(item) {
+ do_check_eq(item.version, 0.1);
+ do_check_false(item.isCompatible);
+
+ item.findUpdates({
+ onUpdateFinished: function(addon) {
+ do_check_false(item.isCompatible);
+
+ testserver.stop(do_test_finished);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug324121.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug324121.js
new file mode 100644
index 000000000..84b6c6189
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug324121.js
@@ -0,0 +1,178 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+// Get the HTTP server.
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver;
+
+var next_test = null;
+var gItemsNotChecked =[];
+
+var ADDONS = [ {id: "bug324121_1@tests.mozilla.org",
+ addon: "test_bug324121_1",
+ shouldCheck: false },
+ {id: "bug324121_2@tests.mozilla.org",
+ addon: "test_bug324121_2",
+ shouldCheck: true },
+ {id: "bug324121_3@tests.mozilla.org",
+ addon: "test_bug324121_3",
+ shouldCheck: true },
+ {id: "bug324121_4@tests.mozilla.org",
+ addon: "test_bug324121_4",
+ shouldCheck: true },
+ {id: "bug324121_5@tests.mozilla.org",
+ addon: "test_bug324121_5",
+ shouldCheck: false },
+ {id: "bug324121_6@tests.mozilla.org",
+ addon: "test_bug324121_6",
+ shouldCheck: true },
+ {id: "bug324121_7@tests.mozilla.org",
+ addon: "test_bug324121_7",
+ shouldCheck: true },
+ {id: "bug324121_8@tests.mozilla.org",
+ addon: "test_bug324121_8",
+ shouldCheck: true },
+ {id: "bug324121_9@tests.mozilla.org",
+ addon: "test_bug324121_9",
+ shouldCheck: false } ];
+
+// nsIAddonUpdateCheckListener
+var updateListener = {
+ pendingCount: 0,
+
+ onUpdateAvailable: function onAddonUpdateEnded(aAddon) {
+ switch (aAddon.id) {
+ // add-on disabled - should not happen
+ case "bug324121_1@tests.mozilla.org":
+ // app id already compatible - should not happen
+ case "bug324121_5@tests.mozilla.org":
+ // toolkit id already compatible - should not happen
+ case "bug324121_9@tests.mozilla.org":
+ do_throw("Should not have seen an update check for " + aAddon.id);
+ break;
+
+ // app id incompatible update available
+ case "bug324121_3@tests.mozilla.org":
+ // update rdf not found
+ case "bug324121_4@tests.mozilla.org":
+ // toolkit id incompatible update available
+ case "bug324121_7@tests.mozilla.org":
+ // update rdf not found
+ case "bug324121_8@tests.mozilla.org":
+ do_throw("Should be no update available for " + aAddon.id);
+ break;
+
+ // Updates available
+ case "bug324121_2@tests.mozilla.org":
+ case "bug324121_6@tests.mozilla.org":
+ break;
+
+ default:
+ do_throw("Update check for unknown " + aAddon.id);
+ }
+
+ // pos should always be >= 0 so just let this throw if this fails
+ var pos = gItemsNotChecked.indexOf(aAddon.id);
+ gItemsNotChecked.splice(pos, 1);
+ },
+
+ onNoUpdateAvailable: function onNoUpdateAvailable(aAddon) {
+ switch (aAddon.id) {
+ // add-on disabled - should not happen
+ case "bug324121_1@tests.mozilla.org":
+ // app id already compatible - should not happen
+ case "bug324121_5@tests.mozilla.org":
+ // toolkit id already compatible - should not happen
+ case "bug324121_9@tests.mozilla.org":
+ do_throw("Should not have seen an update check for " + aAddon.id);
+ break;
+
+ // app id incompatible update available
+ case "bug324121_3@tests.mozilla.org":
+ // update rdf not found
+ case "bug324121_4@tests.mozilla.org":
+ // toolkit id incompatible update available
+ case "bug324121_7@tests.mozilla.org":
+ // update rdf not found
+ case "bug324121_8@tests.mozilla.org":
+ break;
+
+ // Updates available
+ case "bug324121_2@tests.mozilla.org":
+ case "bug324121_6@tests.mozilla.org":
+ do_throw("Should be an update available for " + aAddon.id);
+ break;
+
+ default:
+ do_throw("Update check for unknown " + aAddon.id);
+ }
+
+ // pos should always be >= 0 so just let this throw if this fails
+ var pos = gItemsNotChecked.indexOf(aAddon.id);
+ gItemsNotChecked.splice(pos, 1);
+ },
+
+ onUpdateFinished: function onUpdateFinished(aAddon) {
+ if (--this.pendingCount == 0)
+ test_complete();
+ }
+};
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ const dataDir = do_get_file("data");
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/data/", dataDir);
+ testserver.start(4444);
+
+ startupManager();
+
+ installAllFiles(ADDONS.map(a => do_get_addon(a.addon)), function() {
+ restartManager();
+ AddonManager.getAddonByID(ADDONS[0].id, callback_soon(function(firstAddon) {
+ do_check_true(firstAddon);
+ firstAddon.userDisabled = true;
+ restartManager();
+
+ AddonManager.getAddonsByTypes(["extension"], function(installedItems) {
+ var items = [];
+
+ for (let addon of ADDONS) {
+ for (let installedItem of installedItems) {
+ if (addon.id != installedItem.id)
+ continue;
+ if (installedItem.userDisabled)
+ continue;
+
+ if (addon.shouldCheck == installedItem.isCompatibleWith("3", "3")) {
+ do_throw(installedItem.id + " had the wrong compatibility: " +
+ installedItem.isCompatibleWith("3", "3"));
+ }
+
+ if (addon.shouldCheck) {
+ gItemsNotChecked.push(addon.id);
+ updateListener.pendingCount++;
+ installedItem.findUpdates(updateListener,
+ AddonManager.UPDATE_WHEN_USER_REQUESTED,
+ "3", "3");
+ }
+ }
+ }
+ });
+ }));
+ });
+}
+
+function test_complete() {
+ do_check_eq(gItemsNotChecked.length, 0);
+ testserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug335238.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug335238.js
new file mode 100644
index 000000000..251bdca70
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug335238.js
@@ -0,0 +1,173 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+// This is the data we expect to see sent as part of the update url.
+var EXPECTED = [
+ {
+ id: "bug335238_1@tests.mozilla.org",
+ version: "1.3.4",
+ maxAppVersion: "5",
+ status: "userEnabled",
+ appId: "xpcshell@tests.mozilla.org",
+ appVersion: "1",
+ appOs: "XPCShell",
+ appAbi: "noarch-spidermonkey",
+ locale: "en-US",
+ reqVersion: "2"
+ },
+ {
+ id: "bug335238_2@tests.mozilla.org",
+ version: "28at",
+ maxAppVersion: "7",
+ status: "userDisabled",
+ appId: "xpcshell@tests.mozilla.org",
+ appVersion: "1",
+ appOs: "XPCShell",
+ appAbi: "noarch-spidermonkey",
+ locale: "en-US",
+ reqVersion: "2"
+ },
+ {
+ id: "bug335238_3@tests.mozilla.org",
+ version: "58",
+ maxAppVersion: "*",
+ status: "userDisabled,softblocked",
+ appId: "xpcshell@tests.mozilla.org",
+ appVersion: "1",
+ appOs: "XPCShell",
+ appAbi: "noarch-spidermonkey",
+ locale: "en-US",
+ reqVersion: "2"
+ },
+ {
+ id: "bug335238_4@tests.mozilla.org",
+ version: "4",
+ maxAppVersion: "2+",
+ status: "userEnabled,blocklisted",
+ appId: "xpcshell@tests.mozilla.org",
+ appVersion: "1",
+ appOs: "XPCShell",
+ appAbi: "noarch-spidermonkey",
+ locale: "en-US",
+ reqVersion: "2"
+ }
+];
+
+var ADDONS = [
+ {id: "bug335238_1@tests.mozilla.org",
+ addon: "test_bug335238_1"},
+ {id: "bug335238_2@tests.mozilla.org",
+ addon: "test_bug335238_2"},
+ {id: "bug335238_3@tests.mozilla.org",
+ addon: "test_bug335238_3"},
+ {id: "bug335238_4@tests.mozilla.org",
+ addon: "test_bug335238_4"}
+];
+
+// This is a replacement for the blocklist service
+var BlocklistService = {
+ getAddonBlocklistState: function(aAddon, aAppVersion, aToolkitVersion) {
+ if (aAddon.id == "bug335238_3@tests.mozilla.org")
+ return Ci.nsIBlocklistService.STATE_SOFTBLOCKED;
+ if (aAddon.id == "bug335238_4@tests.mozilla.org")
+ return Ci.nsIBlocklistService.STATE_BLOCKED;
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ getPluginBlocklistState: function(aPlugin, aVersion, aAppVersion, aToolkitVersion) {
+ return Ci.nsIBlocklistService.STATE_NOT_BLOCKED;
+ },
+
+ isAddonBlocklisted: function(aAddon, aAppVersion, aToolkitVersion) {
+ return this.getAddonBlocklistState(aAddon, aAppVersion, aToolkitVersion) ==
+ Ci.nsIBlocklistService.STATE_BLOCKED;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIBlocklistService)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/extensions/blocklist;1", BlocklistService);
+
+var server;
+
+var updateListener = {
+ pendingCount: 0,
+
+ onUpdateAvailable: function(aAddon) {
+ do_throw("Should not have seen an update for " + aAddon.id);
+ },
+
+ onUpdateFinished: function() {
+ if (--this.pendingCount == 0)
+ server.stop(do_test_finished);
+ }
+}
+
+var requestHandler = {
+ handle: function(metadata, response)
+ {
+ var expected = EXPECTED[metadata.path.substring(1)];
+ var params = metadata.queryString.split("&");
+ do_check_eq(params.length, 10);
+ for (var k in params) {
+ var pair = params[k].split("=");
+ var name = decodeURIComponent(pair[0]);
+ var value = decodeURIComponent(pair[1]);
+ do_check_eq(expected[name], value);
+ }
+ response.setStatusLine(metadata.httpVersion, 404, "Not Found");
+ }
+}
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ server = new HttpServer();
+ server.registerPathHandler("/0", requestHandler);
+ server.registerPathHandler("/1", requestHandler);
+ server.registerPathHandler("/2", requestHandler);
+ server.registerPathHandler("/3", requestHandler);
+ server.start(4444);
+
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "en-US");
+
+ startupManager();
+ installAllFiles(ADDONS.map(a => do_get_addon(a.addon)), function() {
+
+ restartManager();
+ AddonManager.getAddonByID(ADDONS[1].id, callback_soon(function(addon) {
+ do_check_true(!(!addon));
+ addon.userDisabled = true;
+ restartManager();
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(installedItems) {
+ installedItems.forEach(function(item) {
+ updateListener.pendingCount++;
+ item.findUpdates(updateListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+ }));
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug371495.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug371495.js
new file mode 100644
index 000000000..43656f126
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug371495.js
@@ -0,0 +1,35 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const ADDON = "test_bug371495";
+const ID = "bug371495@tests.mozilla.org";
+
+function run_test()
+{
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1");
+
+ // Install test add-on
+ startupManager();
+ installAllFiles([do_get_addon(ADDON)], function() {
+ AddonManager.getAddonByID(ID, callback_soon(function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "Test theme");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, callback_soon(function(addon2) {
+ do_check_neq(addon2, null);
+ do_check_eq(addon2.optionsURL, null);
+ do_check_eq(addon2.aboutURL, null);
+
+ do_execute_soon(do_test_finished);
+ }));
+ }));
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug384052.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug384052.js
new file mode 100644
index 000000000..aeaaf3d8f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug384052.js
@@ -0,0 +1,103 @@
+const CLASS_ID = Components.ID("{12345678-1234-1234-1234-123456789abc}");
+const CONTRACT_ID = "@mozilla.org/test-parameter-source;1";
+
+// Get and create the HTTP server.
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+var gTestURL = "http://127.0.0.1:" + gPort + "/update.rdf?itemID=%ITEM_ID%&custom1=%CUSTOM1%&custom2=%CUSTOM2%";
+var gExpectedQuery = "itemID=test@mozilla.org&custom1=custom_parameter_1&custom2=custom_parameter_2";
+var gSeenExpectedURL = false;
+
+var gComponentRegistrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
+var gCategoryManager = AM_Cc["@mozilla.org/categorymanager;1"].getService(AM_Ci.nsICategoryManager);
+
+// Factory for our parameter handler
+var paramHandlerFactory = {
+ QueryInterface: function(iid) {
+ if (iid.equals(AM_Ci.nsIFactory) || iid.equals(AM_Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ createInstance: function(outer, iid) {
+ var bag = AM_Cc["@mozilla.org/hash-property-bag;1"].
+ createInstance(AM_Ci.nsIWritablePropertyBag);
+ bag.setProperty("CUSTOM1", "custom_parameter_1");
+ bag.setProperty("CUSTOM2", "custom_parameter_2");
+ return bag.QueryInterface(iid);
+ }
+};
+
+function initTest()
+{
+ do_test_pending();
+ // Setup extension manager
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ // Configure the HTTP server.
+ testserver.registerPathHandler("/update.rdf", function(aRequest, aResponse) {
+ gSeenExpectedURL = aRequest.queryString == gExpectedQuery;
+ aResponse.setStatusLine(null, 404, "Not Found");
+ });
+
+ // Register our parameter handlers
+ gComponentRegistrar.registerFactory(CLASS_ID, "Test component", CONTRACT_ID, paramHandlerFactory);
+ gCategoryManager.addCategoryEntry("extension-update-params", "CUSTOM1", CONTRACT_ID, false, false);
+ gCategoryManager.addCategoryEntry("extension-update-params", "CUSTOM2", CONTRACT_ID, false, false);
+
+ // Install a test extension into the profile
+ let dir = gProfD.clone();
+ dir.append("extensions");
+ writeInstallRDFForExtension({
+ id: "test@mozilla.org",
+ version: "1.0",
+ name: "Test extension",
+ updateURL: gTestURL,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ }, dir);
+
+ startupManager();
+}
+
+function shutdownTest()
+{
+ shutdownManager();
+
+ gComponentRegistrar.unregisterFactory(CLASS_ID, paramHandlerFactory);
+ gCategoryManager.deleteCategoryEntry("extension-update-params", "CUSTOM1", false);
+ gCategoryManager.deleteCategoryEntry("extension-update-params", "CUSTOM2", false);
+
+ do_test_finished();
+}
+
+function run_test()
+{
+ initTest();
+
+ AddonManager.getAddonByID("test@mozilla.org", function(item) {
+ // Initiate update
+ item.findUpdates({
+ onCompatibilityUpdateAvailable: function(addon) {
+ do_throw("Should not have seen a compatibility update");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ do_throw("Should not have seen an available update");
+ },
+
+ onUpdateFinished: function(addon, error) {
+ do_check_eq(error, AddonManager.UPDATE_STATUS_DOWNLOAD_ERROR);
+ do_check_true(gSeenExpectedURL);
+ do_execute_soon(shutdownTest);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug393285.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug393285.js
new file mode 100644
index 000000000..ebc330cdd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug393285.js
@@ -0,0 +1,316 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_bug393285.xml", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var addonIDs = ["test_bug393285_1@tests.mozilla.org",
+ "test_bug393285_2@tests.mozilla.org",
+ "test_bug393285_3a@tests.mozilla.org",
+ "test_bug393285_3b@tests.mozilla.org",
+ "test_bug393285_4@tests.mozilla.org",
+ "test_bug393285_5@tests.mozilla.org",
+ "test_bug393285_6@tests.mozilla.org",
+ "test_bug393285_7@tests.mozilla.org",
+ "test_bug393285_8@tests.mozilla.org",
+ "test_bug393285_9@tests.mozilla.org",
+ "test_bug393285_10@tests.mozilla.org",
+ "test_bug393285_11@tests.mozilla.org",
+ "test_bug393285_12@tests.mozilla.org",
+ "test_bug393285_13@tests.mozilla.org",
+ "test_bug393285_14@tests.mozilla.org"];
+
+// A window watcher to deal with the blocklist UI dialog.
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = args.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+
+function load_blocklist(aFile, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_1@tests.mozilla.org",
+ name: "extension 1",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_2@tests.mozilla.org",
+ name: "extension 2",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_3a@tests.mozilla.org",
+ name: "extension 3a",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_3b@tests.mozilla.org",
+ name: "extension 3b",
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_4@tests.mozilla.org",
+ name: "extension 4",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_5@tests.mozilla.org",
+ name: "extension 5",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_6@tests.mozilla.org",
+ name: "extension 6",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_7@tests.mozilla.org",
+ name: "extension 7",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_8@tests.mozilla.org",
+ name: "extension 8",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_9@tests.mozilla.org",
+ name: "extension 9",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_10@tests.mozilla.org",
+ name: "extension 10",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_11@tests.mozilla.org",
+ name: "extension 11",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_12@tests.mozilla.org",
+ name: "extension 12",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_13@tests.mozilla.org",
+ name: "extension 13",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_14@tests.mozilla.org",
+ name: "extension 14",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(addonIDs, function(addons) {
+ for (let addon of addons) {
+ do_check_eq(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ }
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ load_blocklist("test_bug393285.xml", function() {
+ restartManager();
+
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsIBlocklistService);
+
+ AddonManager.getAddonsByIDs(addonIDs,
+ function([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10,
+ a11, a12, a13, a14, a15]) {
+ // No info in blocklist, shouldn't be blocked
+ do_check_false(blocklist.isAddonBlocklisted(a1, "1", "1.9"));
+
+ // Should always be blocked
+ do_check_true(blocklist.isAddonBlocklisted(a2, "1", "1.9"));
+
+ // Only version 1 should be blocked
+ do_check_true(blocklist.isAddonBlocklisted(a3, "1", "1.9"));
+ do_check_false(blocklist.isAddonBlocklisted(a4, "1", "1.9"));
+
+ // Should be blocked for app version 1
+ do_check_true(blocklist.isAddonBlocklisted(a5, "1", "1.9"));
+ do_check_false(blocklist.isAddonBlocklisted(a5, "2", "1.9"));
+
+ // Not blocklisted because we are a different OS
+ do_check_false(blocklist.isAddonBlocklisted(a6, "2", "1.9"));
+
+ // Blocklisted based on OS
+ do_check_true(blocklist.isAddonBlocklisted(a7, "2", "1.9"));
+ do_check_true(blocklist.isAddonBlocklisted(a8, "2", "1.9"));
+
+ // Not blocklisted because we are a different ABI
+ do_check_false(blocklist.isAddonBlocklisted(a9, "2", "1.9"));
+
+ // Blocklisted based on ABI
+ do_check_true(blocklist.isAddonBlocklisted(a10, "2", "1.9"));
+ do_check_true(blocklist.isAddonBlocklisted(a11, "2", "1.9"));
+
+ // Doesnt match both os and abi so not blocked
+ do_check_false(blocklist.isAddonBlocklisted(a12, "2", "1.9"));
+ do_check_false(blocklist.isAddonBlocklisted(a13, "2", "1.9"));
+ do_check_false(blocklist.isAddonBlocklisted(a14, "2", "1.9"));
+
+ // Matches both os and abi so blocked
+ do_check_true(blocklist.isAddonBlocklisted(a15, "2", "1.9"));
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug394300.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug394300.js
new file mode 100644
index 000000000..bd393b91c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug394300.js
@@ -0,0 +1,56 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+
+// nsIAddonUpdateCheckListener implementation
+var updateListener = {
+ _count: 0,
+
+ onUpdateAvailable: function onAddonUpdateEnded(aAddon, aInstall) {
+ do_check_eq(aInstall.version, 10);
+ },
+
+ onNoUpdateAvailable: function onNoUpdateAvailable(aAddon) {
+ do_throw("Expected an available update for " + aAddon.id);
+ },
+
+ onUpdateFinished: function onUpdateFinished() {
+ if (++this._count == 2)
+ server.stop(do_test_finished);
+ },
+}
+
+function run_test()
+{
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+ startupManager();
+
+ installAllFiles([do_get_addon("test_bug394300_1"),
+ do_get_addon("test_bug394300_2")], function() {
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug394300_1@tests.mozilla.org",
+ "bug394300_2@tests.mozilla.org"], function(updates) {
+
+ do_check_neq(updates[0], null);
+ do_check_neq(updates[1], null);
+
+ server = new HttpServer();
+ server.registerDirectory("/", do_get_file("data"));
+ server.start(4444);
+
+ updates[0].findUpdates(updateListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ updates[1].findUpdates(updateListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug397778.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug397778.js
new file mode 100644
index 000000000..aa18a6946
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug397778.js
@@ -0,0 +1,117 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const ADDON = "test_bug397778";
+const ID = "bug397778@tests.mozilla.org";
+
+function run_test()
+{
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1");
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+
+ // Install test add-on
+ startupManager();
+ installAllFiles([do_get_addon(ADDON)], function() {
+ restartManager();
+
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ AddonManager.getAddonByID(ID, callback_soon(function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "fr Name");
+ do_check_eq(addon.description, "fr Description");
+
+ // Disable item
+ addon.userDisabled = true;
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(newAddon) {
+ do_check_neq(newAddon, null);
+ do_check_eq(newAddon.name, "fr Name");
+
+ do_execute_soon(run_test_2);
+ });
+ }));
+}
+
+function run_test_2() {
+ // Change locale. The more specific de-DE is the best match
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "de");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "de-DE Name");
+ do_check_eq(addon.description, null);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+function run_test_3() {
+ // Change locale. Locale case should have no effect
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "DE-de");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "de-DE Name");
+ do_check_eq(addon.description, null);
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+function run_test_4() {
+ // Change locale. es-ES should closely match
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "es-AR");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "es-ES Name");
+ do_check_eq(addon.description, "es-ES Description");
+
+ do_execute_soon(run_test_5);
+ });
+}
+
+function run_test_5() {
+ // Change locale. Either zh-CN or zh-TW could match
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "zh");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ if (addon.name != "zh-TW Name" && addon.name != "zh-CN Name")
+ do_throw("zh matched to " + addon.name);
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+function run_test_6() {
+ // Unknown locale should try to match against en-US as well. Of en,en-GB
+ // en should match as being less specific
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "nl-NL");
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "en Name");
+ do_check_eq(addon.description, "en Description");
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug406118.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug406118.js
new file mode 100644
index 000000000..e22ab87c9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug406118.js
@@ -0,0 +1,155 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var addonIDs = ["test_bug393285_1@tests.mozilla.org",
+ "test_bug393285_2@tests.mozilla.org",
+ "test_bug393285_3a@tests.mozilla.org",
+ "test_bug393285_4@tests.mozilla.org"];
+
+var {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/test_bug393285.xml", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// A window watcher to deal with the blocklist UI dialog.
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = args.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function load_blocklist(aFile, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + aFile);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_1@tests.mozilla.org",
+ name: "extension 1",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_2@tests.mozilla.org",
+ name: "extension 2",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_3a@tests.mozilla.org",
+ name: "extension 3a",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "test_bug393285_4@tests.mozilla.org",
+ name: "extension 4",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(addonIDs, function(addons) {
+ for (let addon of addons) {
+ do_check_eq(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ }
+ run_test_1();
+ });
+}
+
+function run_test_1() {
+ load_blocklist("test_bug393285.xml", function() {
+ restartManager();
+
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsIBlocklistService);
+
+ AddonManager.getAddonsByIDs(addonIDs,
+ function([a1, a2, a3, a4]) {
+ // No info in blocklist, shouldn't be blocked
+ do_check_false(blocklist.isAddonBlocklisted(a1, null, null));
+
+ // All these should be blocklisted for the current app.
+ do_check_true(blocklist.isAddonBlocklisted(a2, null, null));
+ do_check_true(blocklist.isAddonBlocklisted(a3, null, null));
+ do_check_true(blocklist.isAddonBlocklisted(a4, null, null));
+
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug424262.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug424262.js
new file mode 100644
index 000000000..8b29e15a5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug424262.js
@@ -0,0 +1,62 @@
+/* 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/.
+ */
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+
+const PREF_GETADDONS_GETRECOMMENDED = "extensions.getAddons.recommended.url";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+var RESULTS = [
+ null,
+ null,
+ 0,
+ 2,
+ 4,
+ 5,
+ 5,
+ 5
+];
+
+var RecommendedCallback = {
+ searchSucceeded: function(addons, length, total) {
+ dump("loaded");
+ // Search is complete
+ do_check_eq(length, RESULTS.length);
+
+ for (var i = 0; i < length; i++) {
+ if (addons[i].averageRating != RESULTS[i])
+ do_throw("Rating for " + addons[i].id + " was " + addons[i].averageRating + ", should have been " + RESULTS[i]);
+ }
+ server.stop(do_test_finished);
+ },
+
+ searchFailed: function() {
+ server.stop(do_test_finished);
+ do_throw("Recommended results failed");
+ }
+};
+
+function run_test()
+{
+ // EM needs to be running.
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+ startupManager();
+
+ server = new HttpServer();
+ server.start(-1);
+ gPort = server.identity.primaryPort;
+ mapFile("/data/test_bug424262.xml", server);
+
+ // Point the addons repository to the test server
+ Services.prefs.setCharPref(PREF_GETADDONS_GETRECOMMENDED, "http://localhost:" +
+ gPort + "/data/test_bug424262.xml");
+
+ do_check_neq(AddonRepository, null);
+
+ do_test_pending();
+ // Pull some results.
+ AddonRepository.retrieveRecommendedAddons(RESULTS.length, RecommendedCallback);
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug425657.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug425657.js
new file mode 100644
index 000000000..f11a942fb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug425657.js
@@ -0,0 +1,27 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const ADDON = "test_bug425657";
+const ID = "bug425657@tests.mozilla.org";
+
+function run_test()
+{
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1");
+
+ // Install test add-on
+ startupManager();
+ installAllFiles([do_get_addon(ADDON)], function() {
+ restartManager();
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "Deutsches W\u00f6rterbuch");
+ do_check_eq(addon.name.length, 20);
+
+ do_execute_soon(do_test_finished);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug430120.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug430120.js
new file mode 100644
index 000000000..e13f36a7c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug430120.js
@@ -0,0 +1,135 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const BLOCKLIST_TIMER = "blocklist-background-update-timer";
+const PREF_BLOCKLIST_URL = "extensions.blocklist.url";
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+const PREF_APP_DISTRIBUTION = "distribution.id";
+const PREF_APP_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_GENERAL_USERAGENT_LOCALE = "general.useragent.locale";
+const CATEGORY_UPDATE_TIMER = "update-timer";
+
+// Get the HTTP server.
+Components.utils.import("resource://testing-common/httpd.js");
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+var testserver;
+var gOSVersion;
+var gBlocklist;
+
+// This is a replacement for the timer service so we can trigger timers
+var timerService = {
+
+ hasTimer: function(id) {
+ var catMan = Components.classes["@mozilla.org/categorymanager;1"]
+ .getService(Components.interfaces.nsICategoryManager);
+ var entries = catMan.enumerateCategory(CATEGORY_UPDATE_TIMER);
+ while (entries.hasMoreElements()) {
+ var entry = entries.getNext().QueryInterface(Components.interfaces.nsISupportsCString).data;
+ var value = catMan.getCategoryEntry(CATEGORY_UPDATE_TIMER, entry);
+ var timerID = value.split(",")[2];
+ if (id == timerID) {
+ return true;
+ }
+ }
+ return false;
+ },
+
+ fireTimer: function(id) {
+ gBlocklist.QueryInterface(Components.interfaces.nsITimerCallback).notify(null);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Components.interfaces.nsIUpdateTimerManager)
+ || iid.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/updates/timer-manager;1", timerService);
+
+function failHandler(metadata, response) {
+ do_throw("Should not have attempted to retrieve the blocklist when it is disabled");
+}
+
+function pathHandler(metadata, response) {
+ var ABI = "noarch-spidermonkey";
+ // the blacklist service special-cases ABI for Universal binaries,
+ // so do the same here.
+ if ("@mozilla.org/xpcom/mac-utils;1" in Components.classes) {
+ var macutils = Components.classes["@mozilla.org/xpcom/mac-utils;1"]
+ .getService(Components.interfaces.nsIMacUtils);
+ if (macutils.isUniversalBinary)
+ ABI += "-u-" + macutils.architecturesInBinary;
+ }
+ do_check_eq(metadata.queryString,
+ "xpcshell@tests.mozilla.org&1&XPCShell&1&" +
+ gAppInfo.appBuildID + "&" +
+ "XPCShell_" + ABI + "&locale&updatechannel&" +
+ gOSVersion + "&1.9&distribution&distribution-version");
+ gBlocklist.observe(null, "quit-application", "");
+ gBlocklist.observe(null, "xpcom-shutdown", "");
+ testserver.stop(do_test_finished);
+}
+
+function run_test() {
+ var osVersion;
+ var sysInfo = Components.classes["@mozilla.org/system-info;1"]
+ .getService(Components.interfaces.nsIPropertyBag2);
+ try {
+ osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+ if (osVersion) {
+ try {
+ osVersion += " (" + sysInfo.getProperty("secondaryLibrary") + ")";
+ }
+ catch (e) {
+ }
+ gOSVersion = encodeURIComponent(osVersion);
+ }
+ }
+ catch (e) {
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ testserver = new HttpServer();
+ testserver.registerPathHandler("/1", failHandler);
+ testserver.registerPathHandler("/2", pathHandler);
+ testserver.start(-1);
+ gPort = testserver.identity.primaryPort;
+
+ // Initialise the blocklist service
+ gBlocklist = Components.classes["@mozilla.org/extensions/blocklist;1"]
+ .getService(Components.interfaces.nsIBlocklistService)
+ .QueryInterface(Components.interfaces.nsIObserver);
+ gBlocklist.observe(null, "profile-after-change", "");
+
+ do_check_true(timerService.hasTimer(BLOCKLIST_TIMER));
+
+ do_test_pending();
+
+ // This should have no effect as the blocklist is disabled
+ Services.prefs.setCharPref(PREF_BLOCKLIST_URL, "http://localhost:" + gPort + "/1");
+ Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, false);
+ timerService.fireTimer(BLOCKLIST_TIMER);
+
+ // Some values have to be on the default branch to work
+ var defaults = Services.prefs.QueryInterface(Components.interfaces.nsIPrefService)
+ .getDefaultBranch(null);
+ defaults.setCharPref(PREF_APP_UPDATE_CHANNEL, "updatechannel");
+ defaults.setCharPref(PREF_APP_DISTRIBUTION, "distribution");
+ defaults.setCharPref(PREF_APP_DISTRIBUTION_VERSION, "distribution-version");
+ defaults.setCharPref(PREF_GENERAL_USERAGENT_LOCALE, "locale");
+
+ // This should correctly escape everything
+ Services.prefs.setCharPref(PREF_BLOCKLIST_URL, "http://localhost:" + gPort + "/2?" +
+ "%APP_ID%&%APP_VERSION%&%PRODUCT%&%VERSION%&%BUILD_ID%&" +
+ "%BUILD_TARGET%&%LOCALE%&%CHANNEL%&" +
+ "%OS_VERSION%&%PLATFORM_VERSION%&%DISTRIBUTION%&%DISTRIBUTION_VERSION%");
+ Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, true);
+ timerService.fireTimer(BLOCKLIST_TIMER);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug449027.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug449027.js
new file mode 100644
index 000000000..1512a7f92
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug449027.js
@@ -0,0 +1,429 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+var ADDONS = [{
+ id: "test_bug449027_1@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 1",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_2@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 2",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_3@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 3",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_4@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 4",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_5@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 5",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_6@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 6",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_7@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 7",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_8@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 8",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_9@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 9",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_10@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 10",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_11@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 11",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_12@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 12",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_13@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 13",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_14@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 14",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_15@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 15",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_16@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 16",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_17@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 17",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_18@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 18",
+ version: "5",
+ start: false,
+ appBlocks: false,
+ toolkitBlocks: false
+}, {
+ id: "test_bug449027_19@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 19",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_20@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 20",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_21@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 21",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_22@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 22",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_23@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 23",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_24@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 24",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}, {
+ id: "test_bug449027_25@tests.mozilla.org",
+ name: "Bug 449027 Addon Test 25",
+ version: "5",
+ start: false,
+ appBlocks: true,
+ toolkitBlocks: true
+}];
+
+function MockPluginTag(name, version, start, appBlocks, toolkitBlocks)
+{
+ this.name = name;
+ this.version = version;
+ this.start = start;
+ this.appBlocks = appBlocks;
+ this.toolkitBlocks = toolkitBlocks;
+}
+Object.defineProperty(MockPluginTag.prototype, "blocklisted", {
+ get: function MockPluginTag_getBlocklisted() {
+ let bls = AM_Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsIBlocklistService);
+ return bls.getPluginBlocklistState(this) == bls.STATE_BLOCKED;
+ }
+});
+
+var PLUGINS = [
+ new MockPluginTag("test_bug449027_1", "5", false, false, false),
+ new MockPluginTag("test_bug449027_2", "5", false, true, false),
+ new MockPluginTag("test_bug449027_3", "5", false, true, false),
+ new MockPluginTag("test_bug449027_4", "5", false, false, false),
+ new MockPluginTag("test_bug449027_5", "5", false, false, false),
+ new MockPluginTag("test_bug449027_6", "5", false, true, false),
+ new MockPluginTag("test_bug449027_7", "5", false, true, false),
+ new MockPluginTag("test_bug449027_8", "5", false, true, false),
+ new MockPluginTag("test_bug449027_9", "5", false, true, false),
+ new MockPluginTag("test_bug449027_10", "5", false, true, false),
+ new MockPluginTag("test_bug449027_11", "5", false, true, false),
+ new MockPluginTag("test_bug449027_12", "5", false, true, false),
+ new MockPluginTag("test_bug449027_13", "5", false, true, false),
+ new MockPluginTag("test_bug449027_14", "5", false, false, false),
+ new MockPluginTag("test_bug449027_15", "5", false, true, true),
+ new MockPluginTag("test_bug449027_16", "5", false, true, true),
+ new MockPluginTag("test_bug449027_17", "5", false, false, false),
+ new MockPluginTag("test_bug449027_18", "5", false, false, false),
+ new MockPluginTag("test_bug449027_19", "5", false, true, true),
+ new MockPluginTag("test_bug449027_20", "5", false, true, true),
+ new MockPluginTag("test_bug449027_21", "5", false, true, true),
+ new MockPluginTag("test_bug449027_22", "5", false, true, true),
+ new MockPluginTag("test_bug449027_23", "5", false, true, true),
+ new MockPluginTag("test_bug449027_24", "5", false, true, true),
+ new MockPluginTag("test_bug449027_25", "5", false, true, true)
+];
+
+var gCallback = null;
+var gTestserver = null;
+var gNewBlocks = [];
+
+// A fake plugin host for the blocklist service to use
+var PluginHost = {
+ getPluginTags: function(countRef) {
+ countRef.value = PLUGINS.length;
+ return PLUGINS;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIPluginHost)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+ do_check_neq(gCallback, null);
+
+ args = args.wrappedJSObject;
+
+ gNewBlocks = [];
+ var list = args.list;
+ for (let listItem of list)
+ gNewBlocks.push(listItem.name + " " + listItem.version);
+
+ // Call the callback after the blocklist has finished up
+ do_timeout(0, gCallback);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function create_addon(addon) {
+ var installrdf = "<?xml version=\"1.0\"?>\n" +
+ "\n" +
+ "<RDF xmlns=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n" +
+ " xmlns:em=\"http://www.mozilla.org/2004/em-rdf#\">\n" +
+ " <Description about=\"urn:mozilla:install-manifest\">\n" +
+ " <em:id>" + addon.id + "</em:id>\n" +
+ " <em:version>" + addon.version + "</em:version>\n" +
+ " <em:targetApplication>\n" +
+ " <Description>\n" +
+ " <em:id>xpcshell@tests.mozilla.org</em:id>\n" +
+ " <em:minVersion>3</em:minVersion>\n" +
+ " <em:maxVersion>3</em:maxVersion>\n" +
+ " </Description>\n" +
+ " </em:targetApplication>\n" +
+ " <em:name>" + addon.name + "</em:name>\n" +
+ " </Description>\n" +
+ "</RDF>\n";
+ var target = gProfD.clone();
+ target.append("extensions");
+ target.append(addon.id);
+ target.append("install.rdf");
+ target.create(target.NORMAL_FILE_TYPE, 0o644);
+ var stream = Components.classes["@mozilla.org/network/file-output-stream;1"]
+ .createInstance(Ci.nsIFileOutputStream);
+ stream.init(target, 0x04 | 0x08 | 0x20, 0o664, 0); // write, create, truncate
+ stream.write(installrdf, installrdf.length);
+ stream.close();
+}
+
+/**
+ * Checks that items are blocklisted correctly according to the current test.
+ * If a lastTest is provided checks that the notification dialog got passed
+ * the newly blocked items compared to the previous test.
+ */
+function check_state(test, lastTest, callback) {
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(addons) {
+ for (var i = 0; i < ADDONS.length; i++) {
+ var blocked = addons[i].blocklistState == Ci.nsIBlocklistService.STATE_BLOCKED;
+ if (blocked != ADDONS[i][test])
+ do_throw("Blocklist state did not match expected for extension " + (i + 1) + ", test " + test);
+ }
+
+ for (i = 0; i < PLUGINS.length; i++) {
+ if (PLUGINS[i].blocklisted != PLUGINS[i][test])
+ do_throw("Blocklist state did not match expected for plugin " + (i + 1) + ", test " + test);
+ }
+
+ if (lastTest) {
+ var expected = 0;
+ for (i = 0; i < ADDONS.length; i++) {
+ if (ADDONS[i][test] && !ADDONS[i][lastTest]) {
+ if (gNewBlocks.indexOf(ADDONS[i].name + " " + ADDONS[i].version) < 0)
+ do_throw("Addon " + (i + 1) + " should have been listed in the blocklist notification for test " + test);
+ expected++;
+ }
+ }
+
+ for (i = 0; i < PLUGINS.length; i++) {
+ if (PLUGINS[i][test] && !PLUGINS[i][lastTest]) {
+ if (gNewBlocks.indexOf(PLUGINS[i].name + " " + PLUGINS[i].version) < 0)
+ do_throw("Plugin " + (i + 1) + " should have been listed in the blocklist notification for test " + test);
+ expected++;
+ }
+ }
+
+ do_check_eq(expected, gNewBlocks.length);
+ }
+ do_execute_soon(callback);
+ });
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/" + file);
+ var blocklist = Components.classes["@mozilla.org/extensions/blocklist;1"]
+ .getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+function run_test() {
+ // Setup for test
+ dump("Setting up tests\n");
+ // Rather than keeping lots of identical add-ons in version control, just
+ // write them into the profile.
+ for (let addon of ADDONS)
+ create_addon(addon);
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ gTestserver = new HttpServer();
+ gTestserver.registerDirectory("/data/", do_get_file("data"));
+ gTestserver.start(-1);
+ gPort = gTestserver.identity.primaryPort;
+
+ do_test_pending();
+ check_test_pt1();
+}
+
+/**
+ * Checks the initial state is correct
+ */
+function check_test_pt1() {
+ dump("Checking pt 1\n");
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(addons) {
+ for (var i = 0; i < ADDONS.length; i++) {
+ if (!addons[i])
+ do_throw("Addon " + (i + 1) + " did not get installed correctly");
+ }
+
+ do_execute_soon(function checkstate1() { check_state("start", null, run_test_pt2); });
+ });
+}
+
+/**
+ * Load the toolkit based blocks
+ */
+function run_test_pt2() {
+ dump("Running test pt 2\n");
+ gCallback = check_test_pt2;
+ load_blocklist("test_bug449027_toolkit.xml");
+}
+
+function check_test_pt2() {
+ dump("Checking pt 2\n");
+ check_state("toolkitBlocks", "start", run_test_pt3);
+}
+
+/**
+ * Load the application based blocks
+ */
+function run_test_pt3() {
+ dump("Running test pt 3\n");
+ gCallback = check_test_pt3;
+ load_blocklist("test_bug449027_app.xml");
+}
+
+function check_test_pt3() {
+ dump("Checking pt 3\n");
+ check_state("appBlocks", "toolkitBlocks", end_test);
+}
+
+function end_test() {
+ gTestserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug455906.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug455906.js
new file mode 100644
index 000000000..06e29b376
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug455906.js
@@ -0,0 +1,517 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+
+// register static files with server and interpolate port numbers in them
+mapFile("/data/bug455906_warn.xml", gTestserver);
+mapFile("/data/bug455906_start.xml", gTestserver);
+mapFile("/data/bug455906_block.xml", gTestserver);
+mapFile("/data/bug455906_empty.xml", gTestserver);
+
+// Workaround for Bug 658720 - URL formatter can leak during xpcshell tests
+const PREF_BLOCKLIST_ITEM_URL = "extensions.blocklist.itemURL";
+Services.prefs.setCharPref(PREF_BLOCKLIST_ITEM_URL, "http://localhost:" + gPort + "/blocklist/%blockID%");
+
+var ADDONS = [{
+ // Tests how the blocklist affects a disabled add-on
+ id: "test_bug455906_1@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 1",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects an enabled add-on
+ id: "test_bug455906_2@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 2",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects an enabled add-on, to be disabled by the notification
+ id: "test_bug455906_3@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 3",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects a disabled add-on that was already warned about
+ id: "test_bug455906_4@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 4",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects an enabled add-on that was already warned about
+ id: "test_bug455906_5@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 5",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects an already blocked add-on
+ id: "test_bug455906_6@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 6",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Tests how the blocklist affects an incompatible add-on
+ id: "test_bug455906_7@tests.mozilla.org",
+ name: "Bug 455906 Addon Test 7",
+ version: "5",
+ appVersion: "2"
+}, {
+ // Spare add-on used to ensure we get a notification when switching lists
+ id: "dummy_bug455906_1@tests.mozilla.org",
+ name: "Dummy Addon 1",
+ version: "5",
+ appVersion: "3"
+}, {
+ // Spare add-on used to ensure we get a notification when switching lists
+ id: "dummy_bug455906_2@tests.mozilla.org",
+ name: "Dummy Addon 2",
+ version: "5",
+ appVersion: "3"
+}];
+
+function MockPlugin(name, version, enabledState) {
+ this.name = name;
+ this.version = version;
+ this.enabledState = enabledState;
+}
+Object.defineProperty(MockPlugin.prototype, "blocklisted", {
+ get: function MockPlugin_getBlocklisted() {
+ let bls = Cc["@mozilla.org/extensions/blocklist;1"].getService(Ci.nsIBlocklistService);
+ return bls.getPluginBlocklistState(this) == bls.STATE_BLOCKED;
+ }
+});
+Object.defineProperty(MockPlugin.prototype, "disabled", {
+ get: function MockPlugin_getDisabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ }
+});
+
+var PLUGINS = [
+ // Tests how the blocklist affects a disabled plugin
+ new MockPlugin("test_bug455906_1", "5", Ci.nsIPluginTag.STATE_DISABLED),
+ // Tests how the blocklist affects an enabled plugin
+ new MockPlugin("test_bug455906_2", "5", Ci.nsIPluginTag.STATE_ENABLED),
+ // Tests how the blocklist affects an enabled plugin, to be disabled by the notification
+ new MockPlugin("test_bug455906_3", "5", Ci.nsIPluginTag.STATE_ENABLED),
+ // Tests how the blocklist affects a disabled plugin that was already warned about
+ new MockPlugin("test_bug455906_4", "5", Ci.nsIPluginTag.STATE_DISABLED),
+ // Tests how the blocklist affects an enabled plugin that was already warned about
+ new MockPlugin("test_bug455906_5", "5", Ci.nsIPluginTag.STATE_ENABLED),
+ // Tests how the blocklist affects an already blocked plugin
+ new MockPlugin("test_bug455906_6", "5", Ci.nsIPluginTag.STATE_ENABLED)
+];
+
+var gNotificationCheck = null;
+var gTestCheck = null;
+
+// A fake plugin host for the blocklist service to use
+var PluginHost = {
+ getPluginTags: function(countRef) {
+ countRef.value = PLUGINS.length;
+ return PLUGINS;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIPluginHost)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, windowArguments) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ if (gNotificationCheck) {
+ var args = windowArguments.wrappedJSObject;
+ gNotificationCheck(args);
+ }
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+
+ // Call the next test after the blocklist has finished up
+ do_timeout(0, gTestCheck);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function create_addon(addon) {
+ var installrdf = "<?xml version=\"1.0\"?>\n" +
+ "\n" +
+ "<RDF xmlns=\"http://www.w3.org/1999/02/22-rdf-syntax-ns#\"\n" +
+ " xmlns:em=\"http://www.mozilla.org/2004/em-rdf#\">\n" +
+ " <Description about=\"urn:mozilla:install-manifest\">\n" +
+ " <em:id>" + addon.id + "</em:id>\n" +
+ " <em:version>" + addon.version + "</em:version>\n" +
+ " <em:targetApplication>\n" +
+ " <Description>\n" +
+ " <em:id>xpcshell@tests.mozilla.org</em:id>\n" +
+ " <em:minVersion>" + addon.appVersion + "</em:minVersion>\n" +
+ " <em:maxVersion>" + addon.appVersion + "</em:maxVersion>\n" +
+ " </Description>\n" +
+ " </em:targetApplication>\n" +
+ " <em:name>" + addon.name + "</em:name>\n" +
+ " </Description>\n" +
+ "</RDF>\n";
+ var target = gProfD.clone();
+ target.append("extensions");
+ target.append(addon.id);
+ target.append("install.rdf");
+ target.create(target.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ var stream = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ stream.init(target,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+ stream.write(installrdf, installrdf.length);
+ stream.close();
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+function check_addon_state(addon) {
+ return addon.userDisabled + "," + addon.softDisabled + "," + addon.appDisabled;
+}
+
+function check_plugin_state(plugin) {
+ return plugin.disabled + "," + plugin.blocklisted;
+}
+
+function create_blocklistURL(blockID) {
+ let url = Services.urlFormatter.formatURLPref(PREF_BLOCKLIST_ITEM_URL);
+ url = url.replace(/%blockID%/g, blockID);
+ return url;
+}
+
+// Performs the initial setup
+function run_test() {
+ // Setup for test
+ dump("Setting up tests\n");
+ // Rather than keeping lots of identical add-ons in version control, just
+ // write them into the profile.
+ for (let addon of ADDONS)
+ create_addon(addon);
+
+ // Copy the initial blocklist into the profile to check add-ons start in the
+ // right state.
+ copyBlocklistToProfile(do_get_file("data/bug455906_start.xml"));
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+ check_test_pt1();
+}
+
+// Before every main test this is the state the add-ons are meant to be in
+function check_initial_state(callback) {
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(addons) {
+ do_check_eq(check_addon_state(addons[0]), "true,false,false");
+ do_check_eq(check_addon_state(addons[1]), "false,false,false");
+ do_check_eq(check_addon_state(addons[2]), "false,false,false");
+ do_check_eq(check_addon_state(addons[3]), "true,true,false");
+ do_check_eq(check_addon_state(addons[4]), "false,false,false");
+ do_check_eq(check_addon_state(addons[5]), "false,false,true");
+ do_check_eq(check_addon_state(addons[6]), "false,false,true");
+
+ do_check_eq(check_plugin_state(PLUGINS[0]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[1]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[2]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[3]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[4]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[5]), "false,true");
+
+ callback();
+ });
+}
+
+// Tests the add-ons were installed and the initial blocklist applied as expected
+function check_test_pt1() {
+ dump("Checking pt 1\n");
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), callback_soon(function(addons) {
+ for (var i = 0; i < ADDONS.length; i++) {
+ if (!addons[i])
+ do_throw("Addon " + (i + 1) + " did not get installed correctly");
+ }
+
+ do_check_eq(check_addon_state(addons[0]), "false,false,false");
+ do_check_eq(check_addon_state(addons[1]), "false,false,false");
+ do_check_eq(check_addon_state(addons[2]), "false,false,false");
+
+ // Warn add-ons should be soft disabled automatically
+ do_check_eq(check_addon_state(addons[3]), "true,true,false");
+ do_check_eq(check_addon_state(addons[4]), "true,true,false");
+
+ // Blocked and incompatible should be app disabled only
+ do_check_eq(check_addon_state(addons[5]), "false,false,true");
+ do_check_eq(check_addon_state(addons[6]), "false,false,true");
+
+ // We've overridden the plugin host so we cannot tell what that would have
+ // initialised the plugins as
+
+ // Put the add-ons into the base state
+ addons[0].userDisabled = true;
+ addons[4].userDisabled = false;
+
+ restartManager();
+ check_initial_state(function() {
+ gNotificationCheck = check_notification_pt2;
+ gTestCheck = check_test_pt2;
+ load_blocklist("bug455906_warn.xml");
+ });
+ }));
+}
+
+function check_notification_pt2(args) {
+ dump("Checking notification pt 2\n");
+ do_check_eq(args.list.length, 4);
+
+ for (let addon of args.list) {
+ if (addon.item instanceof Ci.nsIPluginTag) {
+ switch (addon.item.name) {
+ case "test_bug455906_2":
+ do_check_false(addon.blocked);
+ break;
+ case "test_bug455906_3":
+ do_check_false(addon.blocked);
+ addon.disable = true;
+ break;
+ default:
+ do_throw("Unknown addon: " + addon.item.name);
+ }
+ }
+ else {
+ switch (addon.item.id) {
+ case "test_bug455906_2@tests.mozilla.org":
+ do_check_false(addon.blocked);
+ break;
+ case "test_bug455906_3@tests.mozilla.org":
+ do_check_false(addon.blocked);
+ addon.disable = true;
+ break;
+ default:
+ do_throw("Unknown addon: " + addon.item.id);
+ }
+ }
+ }
+}
+
+function check_test_pt2() {
+ restartManager();
+ dump("Checking results pt 2\n");
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), callback_soon(function(addons) {
+ // Should have disabled this add-on as requested
+ do_check_eq(check_addon_state(addons[2]), "true,true,false");
+ do_check_eq(check_plugin_state(PLUGINS[2]), "true,false");
+
+ // The blocked add-on should have changed to soft disabled
+ do_check_eq(check_addon_state(addons[5]), "true,true,false");
+ do_check_eq(check_addon_state(addons[6]), "true,true,true");
+ do_check_eq(check_plugin_state(PLUGINS[5]), "true,false");
+
+ // These should have been unchanged
+ do_check_eq(check_addon_state(addons[0]), "true,false,false");
+ do_check_eq(check_addon_state(addons[1]), "false,false,false");
+ do_check_eq(check_addon_state(addons[3]), "true,true,false");
+ do_check_eq(check_addon_state(addons[4]), "false,false,false");
+ do_check_eq(check_plugin_state(PLUGINS[0]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[1]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[3]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[4]), "false,false");
+
+ // Back to starting state
+ addons[2].userDisabled = false;
+ addons[5].userDisabled = false;
+ PLUGINS[2].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ PLUGINS[5].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ restartManager();
+ gNotificationCheck = null;
+ gTestCheck = run_test_pt3;
+ load_blocklist("bug455906_start.xml");
+ }));
+}
+
+function run_test_pt3() {
+ restartManager();
+ check_initial_state(function() {
+ gNotificationCheck = check_notification_pt3;
+ gTestCheck = check_test_pt3;
+ load_blocklist("bug455906_block.xml");
+ });
+}
+
+function check_notification_pt3(args) {
+ dump("Checking notification pt 3\n");
+ do_check_eq(args.list.length, 6);
+
+ for (let addon of args.list) {
+ if (addon.item instanceof Ci.nsIPluginTag) {
+ switch (addon.item.name) {
+ case "test_bug455906_2":
+ do_check_true(addon.blocked);
+ break;
+ case "test_bug455906_3":
+ do_check_true(addon.blocked);
+ break;
+ case "test_bug455906_5":
+ do_check_true(addon.blocked);
+ break;
+ default:
+ do_throw("Unknown addon: " + addon.item.name);
+ }
+ }
+ else {
+ switch (addon.item.id) {
+ case "test_bug455906_2@tests.mozilla.org":
+ do_check_true(addon.blocked);
+ break;
+ case "test_bug455906_3@tests.mozilla.org":
+ do_check_true(addon.blocked);
+ break;
+ case "test_bug455906_5@tests.mozilla.org":
+ do_check_true(addon.blocked);
+ break;
+ default:
+ do_throw("Unknown addon: " + addon.item.id);
+ }
+ }
+ }
+}
+
+function check_test_pt3() {
+ restartManager();
+ dump("Checking results pt 3\n");
+
+ let blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsIBlocklistService);
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(addons) {
+ // All should have gained the blocklist state, user disabled as previously
+ do_check_eq(check_addon_state(addons[0]), "true,false,true");
+ do_check_eq(check_addon_state(addons[1]), "false,false,true");
+ do_check_eq(check_addon_state(addons[2]), "false,false,true");
+ do_check_eq(check_addon_state(addons[4]), "false,false,true");
+ do_check_eq(check_plugin_state(PLUGINS[0]), "true,true");
+ do_check_eq(check_plugin_state(PLUGINS[1]), "false,true");
+ do_check_eq(check_plugin_state(PLUGINS[2]), "false,true");
+ do_check_eq(check_plugin_state(PLUGINS[3]), "true,true");
+ do_check_eq(check_plugin_state(PLUGINS[4]), "false,true");
+
+ // Should have gained the blocklist state but no longer be soft disabled
+ do_check_eq(check_addon_state(addons[3]), "false,false,true");
+
+ // Check blockIDs are correct
+ do_check_eq(blocklist.getAddonBlocklistURL(addons[0]), create_blocklistURL(addons[0].id));
+ do_check_eq(blocklist.getAddonBlocklistURL(addons[1]), create_blocklistURL(addons[1].id));
+ do_check_eq(blocklist.getAddonBlocklistURL(addons[2]), create_blocklistURL(addons[2].id));
+ do_check_eq(blocklist.getAddonBlocklistURL(addons[3]), create_blocklistURL(addons[3].id));
+ do_check_eq(blocklist.getAddonBlocklistURL(addons[4]), create_blocklistURL(addons[4].id));
+
+ // All plugins have the same blockID on the test
+ do_check_eq(blocklist.getPluginBlocklistURL(PLUGINS[0]), create_blocklistURL('test_bug455906_plugin'));
+ do_check_eq(blocklist.getPluginBlocklistURL(PLUGINS[1]), create_blocklistURL('test_bug455906_plugin'));
+ do_check_eq(blocklist.getPluginBlocklistURL(PLUGINS[2]), create_blocklistURL('test_bug455906_plugin'));
+ do_check_eq(blocklist.getPluginBlocklistURL(PLUGINS[3]), create_blocklistURL('test_bug455906_plugin'));
+ do_check_eq(blocklist.getPluginBlocklistURL(PLUGINS[4]), create_blocklistURL('test_bug455906_plugin'));
+
+ // Shouldn't be changed
+ do_check_eq(check_addon_state(addons[5]), "false,false,true");
+ do_check_eq(check_addon_state(addons[6]), "false,false,true");
+ do_check_eq(check_plugin_state(PLUGINS[5]), "false,true");
+
+ // Back to starting state
+ gNotificationCheck = null;
+ gTestCheck = run_test_pt4;
+ load_blocklist("bug455906_start.xml");
+ });
+}
+
+function run_test_pt4() {
+ AddonManager.getAddonByID(ADDONS[4].id, callback_soon(function(addon) {
+ addon.userDisabled = false;
+ PLUGINS[4].enabledState = Ci.nsIPluginTag.STATE_ENABLED;
+ restartManager();
+ check_initial_state(function() {
+ gNotificationCheck = check_notification_pt4;
+ gTestCheck = check_test_pt4;
+ load_blocklist("bug455906_empty.xml");
+ });
+ }));
+}
+
+function check_notification_pt4(args) {
+ dump("Checking notification pt 4\n");
+
+ // Should be just the dummy add-on to force this notification
+ do_check_eq(args.list.length, 1);
+ do_check_false(args.list[0].item instanceof Ci.nsIPluginTag);
+ do_check_eq(args.list[0].item.id, "dummy_bug455906_2@tests.mozilla.org");
+}
+
+function check_test_pt4() {
+ restartManager();
+ dump("Checking results pt 4\n");
+
+ AddonManager.getAddonsByIDs(ADDONS.map(a => a.id), function(addons) {
+ // This should have become unblocked
+ do_check_eq(check_addon_state(addons[5]), "false,false,false");
+ do_check_eq(check_plugin_state(PLUGINS[5]), "false,false");
+
+ // Should get re-enabled
+ do_check_eq(check_addon_state(addons[3]), "false,false,false");
+
+ // No change for anything else
+ do_check_eq(check_addon_state(addons[0]), "true,false,false");
+ do_check_eq(check_addon_state(addons[1]), "false,false,false");
+ do_check_eq(check_addon_state(addons[2]), "false,false,false");
+ do_check_eq(check_addon_state(addons[4]), "false,false,false");
+ do_check_eq(check_addon_state(addons[6]), "false,false,true");
+ do_check_eq(check_plugin_state(PLUGINS[0]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[1]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[2]), "false,false");
+ do_check_eq(check_plugin_state(PLUGINS[3]), "true,false");
+ do_check_eq(check_plugin_state(PLUGINS[4]), "false,false");
+
+ finish();
+ });
+}
+
+function finish() {
+ gTestserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug465190.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug465190.js
new file mode 100644
index 000000000..e8e2353e2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug465190.js
@@ -0,0 +1,39 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var installLocation = gProfD.clone();
+installLocation.append("baddir");
+installLocation.create(Components.interfaces.nsIFile.NORMAL_FILE_TYPE, 0o664);
+
+var dirProvider2 = {
+ getFile: function(prop, persistent) {
+ persistent.value = true;
+ if (prop == "XREUSysExt")
+ return installLocation.clone();
+ return null;
+ },
+ QueryInterface: function(iid) {
+ if (iid.equals(Components.interfaces.nsIDirectoryServiceProvider) ||
+ iid.equals(Components.interfaces.nsISupports)) {
+ return this;
+ }
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+};
+Services.dirsvc.QueryInterface(Components.interfaces.nsIDirectoryService)
+ .registerProvider(dirProvider2);
+
+function run_test()
+{
+ var log = gProfD.clone();
+ log.append("extensions.log");
+ do_check_false(log.exists());
+
+ // Setup for test
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1");
+
+ startupManager();
+ do_check_false(log.exists());
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug468528.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug468528.js
new file mode 100644
index 000000000..01c976a17
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug468528.js
@@ -0,0 +1,58 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const nsIBLS = Components.interfaces.nsIBlocklistService;
+
+var PLUGINS = [{
+ // Normal blacklisted plugin, before an invalid regexp
+ name: "test_bug468528_1",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // Normal blacklisted plugin, with an invalid regexp
+ name: "test_bug468528_2",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // Normal blacklisted plugin, after an invalid regexp
+ name: "test_bug468528_3",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // Non-blocklisted plugin
+ name: "test_bug468528_4",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+}];
+
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ // We cannot force the blocklist to update so just copy our test list to the profile
+ copyBlocklistToProfile(do_get_file("data/test_bug468528.xml"));
+
+ var blocklist = Components.classes["@mozilla.org/extensions/blocklist;1"]
+ .getService(nsIBLS);
+
+ // blocked (sanity check)
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+
+ // not blocked - won't match due to invalid regexp
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+
+ // blocked - the invalid regexp for the previous item shouldn't affect this one
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+
+ // not blocked - the previous invalid regexp shouldn't act as a wildcard
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1.js
new file mode 100644
index 000000000..920c3731a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1.js
@@ -0,0 +1,49 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+var ADDONS = [
+ "test_bug470377_1",
+ "test_bug470377_2",
+ "test_bug470377_3",
+ "test_bug470377_4",
+ "test_bug470377_5",
+];
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ server = new HttpServer();
+ server.registerDirectory("/", do_get_file("data/test_bug470377"));
+ server.start(-1);
+
+ startupManager();
+
+ installAllFiles(ADDONS.map(a => do_get_addon(a)), function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_eq(a1, null);
+ do_check_neq(a2, null);
+ do_check_neq(a3, null);
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+
+ server.stop(do_test_finished);
+ });
+ }, true);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1_strictcompat.js
new file mode 100644
index 000000000..6fa11eb39
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_1_strictcompat.js
@@ -0,0 +1,49 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+var ADDONS = [
+ "test_bug470377_1",
+ "test_bug470377_2",
+ "test_bug470377_3",
+ "test_bug470377_4",
+ "test_bug470377_5",
+];
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ server = new HttpServer();
+ server.registerDirectory("/", do_get_file("data/test_bug470377"));
+ server.start(-1);
+
+ startupManager();
+
+ installAllFiles(ADDONS.map(a => do_get_addon(a)), function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_eq(a1, null);
+ do_check_eq(a2, null);
+ do_check_eq(a3, null);
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+
+ server.stop(do_test_finished);
+ });
+ }, true);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_2.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_2.js
new file mode 100644
index 000000000..0c912ceff
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_2.js
@@ -0,0 +1,49 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+var ADDONS = [
+ "test_bug470377_1",
+ "test_bug470377_2",
+ "test_bug470377_3",
+ "test_bug470377_4",
+ "test_bug470377_5",
+];
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ server = new HttpServer();
+ server.registerDirectory("/", do_get_file("data/test_bug470377"));
+ server.start(-1);
+
+ startupManager();
+ AddonManager.checkCompatibility = false;
+
+ installAllFiles(ADDONS.map(a => do_get_addon(a)), function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_eq(a1, null);
+ do_check_neq(a2, null);
+ do_check_neq(a3, null);
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+
+ server.stop(do_test_finished);
+ });
+ }, true);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3.js
new file mode 100644
index 000000000..0d1d30f3b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3.js
@@ -0,0 +1,95 @@
+/* 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/.
+ */
+
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2.2.3", "2");
+
+ // inject the add-ons into the profile
+ var dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_1@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ var source = do_get_file("data/test_bug470377/install_1.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_2@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_2.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_3@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_3.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_4@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_4.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_5@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_5.rdf");
+ source.copyTo(dest, "install.rdf");
+
+ startupManager();
+
+ run_test_1();
+}
+
+function run_test_1() {
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+function run_test_2() {
+ AddonManager.checkCompatibility = false;
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3_strictcompat.js
new file mode 100644
index 000000000..100ea99d7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_3_strictcompat.js
@@ -0,0 +1,94 @@
+/* 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/.
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2.2.3", "2");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+ // inject the add-ons into the profile
+ var dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_1@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ var source = do_get_file("data/test_bug470377/install_1.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_2@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_2.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_3@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_3.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_4@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_4.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = gProfD.clone();
+ dest.append("extensions");
+ dest.append("bug470377_5@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_5.rdf");
+ source.copyTo(dest, "install.rdf");
+
+ startupManager();
+
+ run_test_1();
+}
+
+function run_test_1() {
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+function run_test_2() {
+ AddonManager.checkCompatibility = false;
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_4.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_4.js
new file mode 100644
index 000000000..c51c38b0c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug470377_4.js
@@ -0,0 +1,92 @@
+/* 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/.
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2.1a4", "2");
+
+ // inject the add-ons into the profile
+ var profileDir = gProfD.clone();
+ profileDir.append("extensions");
+ var dest = profileDir.clone();
+ dest.append("bug470377_1@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ var source = do_get_file("data/test_bug470377/install_1.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = profileDir.clone();
+ dest.append("bug470377_2@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_2.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = profileDir.clone();
+ dest.append("bug470377_3@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_3.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = profileDir.clone();
+ dest.append("bug470377_4@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_4.rdf");
+ source.copyTo(dest, "install.rdf");
+ dest = profileDir.clone();
+ dest.append("bug470377_5@tests.mozilla.org");
+ dest.create(Components.interfaces.nsIFile.DIRECTORY_TYPE, 0o755);
+ source = do_get_file("data/test_bug470377/install_5.rdf");
+ source.copyTo(dest, "install.rdf");
+
+ run_test_1();
+}
+
+function run_test_1() {
+ startupManager();
+ AddonManager.checkCompatibility = false;
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+function run_test_2() {
+ AddonManager.checkCompatibility = true;
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug470377_1@tests.mozilla.org",
+ "bug470377_2@tests.mozilla.org",
+ "bug470377_3@tests.mozilla.org",
+ "bug470377_4@tests.mozilla.org",
+ "bug470377_5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_1.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_1.js
new file mode 100644
index 000000000..46b65ffff
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_1.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const nsIBLS = Ci.nsIBlocklistService;
+
+var PLUGINS = [{
+ // blocklisted - default severity
+ name: "test_bug514327_1",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // outdated - severity of "0"
+ name: "test_bug514327_2",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // outdated - severity of "0"
+ name: "test_bug514327_3",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // not blocklisted, not outdated
+ name: "test_bug514327_4",
+ version: "5",
+ disabled: false,
+ blocklisted: false,
+ outdated: false
+}];
+
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ copyBlocklistToProfile(do_get_file("data/test_bug514327_1.xml"));
+
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].getService(nsIBLS);
+
+ // blocked (sanity check)
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+
+ // outdated
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+
+ // outdated
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+
+ // not blocked
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_2.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_2.js
new file mode 100644
index 000000000..261739da4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_2.js
@@ -0,0 +1,41 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+
+const nsIBLS = Ci.nsIBlocklistService;
+
+// Finds the test nsIPluginTag
+function get_test_plugintag() {
+ var host = Cc["@mozilla.org/plugin/host;1"].getService(Ci.nsIPluginHost);
+ var tags = host.getPluginTags();
+ for (let tag of tags) {
+ if (tag.name == "Test Plug-in")
+ return tag;
+ }
+ return null;
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ copyBlocklistToProfile(do_get_file("data/test_bug514327_2.xml"));
+
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].getService(nsIBLS);
+ var prefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+
+ prefs.setBoolPref("plugin.load_flash_only", false);
+
+ var plugin = get_test_plugintag();
+ if (!plugin)
+ do_throw("Plugin tag not found");
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+ do_execute_soon(function() {
+ // should be marked as outdated by the blocklist
+ do_check_true(blocklist.getPluginBlocklistState(plugin, "1", "1.9") == nsIBLS.STATE_OUTDATED);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js
new file mode 100644
index 000000000..634361991
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug514327_3.js
@@ -0,0 +1,139 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://testing-common/httpd.js");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+const nsIBLS = Ci.nsIBlocklistService;
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+var gBlocklist = null;
+var gPrefs = null;
+var gTestserver = null;
+
+var gNextTestPart = null;
+
+
+var PLUGINS = [{
+ // Tests a plugin whose state goes from not-blocked, to outdated
+ name: "test_bug514327_outdated",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+}, {
+ // Used to trigger the blocklist dialog, which indicates the blocklist has updated
+ name: "test_bug514327_1",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+}, {
+ // Used to trigger the blocklist dialog, which indicates the blocklist has updated
+ name: "test_bug514327_2",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+} ];
+
+
+// A fake plugin host for the blocklist service to use
+var PluginHost = {
+ getPluginTags: function(countRef) {
+ countRef.value = PLUGINS.length;
+ return PLUGINS;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIPluginHost)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+ // Should only include one item
+ do_check_eq(args.wrappedJSObject.list.length, 1);
+ // And that item should be the blocked plugin, not the outdated one
+ var item = args.wrappedJSObject.list[0];
+ do_check_true(item.item instanceof Ci.nsIPluginTag);
+ do_check_neq(item.name, "test_bug514327_outdated");
+
+ // Call the next test after the blocklist has finished up
+ do_timeout(0, gNextTestPart);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+
+function do_update_blocklist(aDatafile, aNextPart) {
+ gNextTestPart = aNextPart;
+
+ gPrefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/" + aDatafile);
+ gBlocklist.QueryInterface(Ci.nsITimerCallback).notify(null);
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ gTestserver = new HttpServer();
+ gTestserver.registerDirectory("/data/", do_get_file("data"));
+ gTestserver.start(-1);
+ gPort = gTestserver.identity.primaryPort;
+
+ startupManager();
+
+ // initialize the blocklist with no entries
+ copyBlocklistToProfile(do_get_file("data/test_bug514327_3_empty.xml"));
+
+ gPrefs = Cc["@mozilla.org/preferences-service;1"].getService(Ci.nsIPrefBranch);
+ gBlocklist = Cc["@mozilla.org/extensions/blocklist;1"].getService(nsIBLS);
+
+ // should NOT be marked as outdated by the blocklist
+ do_check_true(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+
+ do_test_pending();
+
+ // update blocklist with data that marks the plugin as outdated
+ do_update_blocklist("test_bug514327_3_outdated_1.xml", test_part_1);
+}
+
+function test_part_1() {
+ // plugin should now be marked as outdated
+ do_check_true(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+
+ // update blocklist with data that marks the plugin as outdated
+ do_update_blocklist("test_bug514327_3_outdated_2.xml", test_part_2);
+}
+
+function test_part_2() {
+ // plugin should still be marked as outdated
+ do_check_true(gBlocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+
+ finish();
+}
+
+function finish() {
+ gTestserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug521905.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug521905.js
new file mode 100644
index 000000000..b507fc100
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug521905.js
@@ -0,0 +1,59 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const ADDON = "test_bug521905";
+const ID = "bug521905@tests.mozilla.org";
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+function run_test() {
+ // This test is only relevant on builds where the version is included in the
+ // checkCompatibility preference name
+ if (isNightlyChannel()) {
+ return;
+ }
+
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2.0pre", "2");
+
+ startupManager();
+ AddonManager.checkCompatibility = false;
+
+ installAllFiles([do_get_addon(ADDON)], function() {
+ restartManager();
+
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_true(addon.isActive);
+
+ do_execute_soon(run_test_1);
+ });
+ });
+}
+
+function run_test_1() {
+ Services.prefs.setBoolPref("extensions.checkCompatibility.2.0pre", true);
+
+ restartManager();
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_false(addon.isActive);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+function run_test_2() {
+ Services.prefs.setBoolPref("extensions.checkCompatibility.2.0p", false);
+
+ restartManager();
+ AddonManager.getAddonByID(ID, function(addon) {
+ do_check_neq(addon, null);
+ do_check_false(addon.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug526598.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug526598.js
new file mode 100644
index 000000000..debf59172
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug526598.js
@@ -0,0 +1,54 @@
+/* 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/.
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ startupManager();
+
+ installAllFiles([do_get_file("data/test_bug526598_1.xpi"),
+ do_get_file("data/test_bug526598_2.xpi")], function() {
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug526598_1@tests.mozilla.org",
+ "bug526598_2@tests.mozilla.org"],
+ callback_soon(function([a1, a2]) {
+
+ do_check_neq(a1, null);
+ do_check_true(a1.hasResource("install.rdf"));
+ let uri = a1.getResourceURI("install.rdf");
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ let file = uri.file;
+ do_check_true(file.exists());
+ do_check_true(file.isReadable());
+ do_check_true(file.isWritable());
+
+ do_check_neq(a2, null);
+ do_check_true(a2.hasResource("install.rdf"));
+ uri = a2.getResourceURI("install.rdf");
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ file = uri.file;
+ do_check_true(file.exists());
+ do_check_true(file.isReadable());
+ do_check_true(file.isWritable());
+
+ a1.uninstall();
+ a2.uninstall();
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["bug526598_1@tests.mozilla.org",
+ "bug526598_2@tests.mozilla.org"],
+ function([newa1, newa2]) {
+ do_check_eq(newa1, null);
+ do_check_eq(newa2, null);
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug541420.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug541420.js
new file mode 100644
index 000000000..b7af5453f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug541420.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ startupManager();
+
+ installAllFiles([do_get_file("data/test_bug541420.xpi")], function() {
+
+ restartManager();
+
+ AddonManager.getAddonByID("bug541420@tests.mozilla.org", function(addon) {
+
+ do_check_neq(addon, null);
+ do_check_true(addon.hasResource("binary"));
+ let uri = addon.getResourceURI("binary");
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ let file = uri.file;
+ do_check_true(file.exists());
+ do_check_true(file.isReadable());
+ do_check_true(file.isWritable());
+
+ // We don't understand executable permissions on Windows since we don't
+ // support NTFS permissions so we don't need to test there. OSX's isExecutable
+ // only tests if the file is an application so it is better to just check the
+ // raw permission bits
+ if (!("nsIWindowsRegKey" in Components.interfaces))
+ do_check_true((file.permissions & 0o100) == 0o100);
+
+ do_execute_soon(do_test_finished);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug542391.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug542391.js
new file mode 100644
index 000000000..aa1bbd53f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug542391.js
@@ -0,0 +1,464 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver;
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gInstallUpdate = false;
+var gCheckUpdates = false;
+
+// This will be called to show the compatibility update dialog.
+var WindowWatcher = {
+ expected: false,
+ args: null,
+
+ openWindow: function(parent, url, name, features, args) {
+ do_check_true(Services.startup.interrupted);
+ do_check_eq(url, URI_EXTENSION_UPDATE_DIALOG);
+ do_check_true(this.expected);
+ this.expected = false;
+ this.args = args.QueryInterface(AM_Ci.nsIVariant);
+
+ var updated = !gCheckUpdates;
+ if (gCheckUpdates) {
+ AddonManager.getAddonByID("override1x2-1x3@tests.mozilla.org", function(a6) {
+ a6.findUpdates({
+ onUpdateFinished: function() {
+ AddonManagerPrivate.removeStartupChange("disabled", "override1x2-1x3@tests.mozilla.org");
+ updated = true;
+ }
+ }, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ });
+ }
+
+ var installed = !gInstallUpdate;
+ if (gInstallUpdate) {
+ // Simulate installing an update while in the dialog
+ installAllFiles([do_get_addon("upgradeable1x2-3_2")], function() {
+ AddonManagerPrivate.removeStartupChange("disabled", "upgradeable1x2-3@tests.mozilla.org");
+ AddonManagerPrivate.addStartupChange("updated", "upgradeable1x2-3@tests.mozilla.org");
+ installed = true;
+ });
+ }
+
+ // The dialog is meant to be opened modally and the install operation can be
+ // asynchronous, so we must spin an event loop (like the modal window does)
+ // until the install is complete
+ let thr = AM_Cc["@mozilla.org/thread-manager;1"].
+ getService(AM_Ci.nsIThreadManager).
+ mainThread;
+
+ while (!installed || !updated)
+ thr.processNextEvent(false);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function check_state_v1([a1, a2, a3, a4, a5, a6]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_eq(a3.version, "1.0");
+
+ do_check_neq(a4, null);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.appDisabled);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.userDisabled);
+ do_check_true(a6.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+}
+
+function check_state_v1_2([a1, a2, a3, a4, a5, a6]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_eq(a3.version, "2.0");
+
+ do_check_neq(a4, null);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.appDisabled);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.userDisabled);
+ do_check_true(a6.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+}
+
+function check_state_v2([a1, a2, a3, a4, a5, a6]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_eq(a3.version, "1.0");
+
+ do_check_neq(a4, null);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.appDisabled);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.userDisabled);
+ do_check_true(a6.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+}
+
+function check_state_v3([a1, a2, a3, a4, a5, a6]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(a2.appDisabled);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_eq(a3.version, "1.0");
+
+ do_check_neq(a4, null);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.appDisabled);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.userDisabled);
+ do_check_true(a6.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+}
+
+function check_state_v3_2([a1, a2, a3, a4, a5, a6]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(a2.appDisabled);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_eq(a3.version, "2.0");
+
+ do_check_neq(a4, null);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.appDisabled);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.userDisabled);
+ do_check_true(a6.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+}
+
+// Install all the test add-ons, disable two of them and "upgrade" the app to
+// version 2 which will appDisable one.
+add_task(function* init() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ Services.prefs.setBoolPref(PREF_EM_SHOW_MISMATCH_UI, true);
+
+ // Add an extension to the profile to make sure the dialog doesn't show up
+ // on new profiles
+ var dest = writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ // Create and configure the HTTP server.
+ testserver = createHttpServer(4444);
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+ startupManager();
+
+ // Remove the add-on we installed directly in the profile directory;
+ // this should show as uninstalled on next restart
+ dest.remove(true);
+
+ // Load up an initial set of add-ons
+ yield promiseInstallAllFiles([do_get_addon("min1max1"),
+ do_get_addon("min1max2"),
+ do_get_addon("upgradeable1x2-3_1"),
+ do_get_addon("min1max3"),
+ do_get_addon("min1max3b"),
+ do_get_addon("override1x2-1x3")]);
+ yield promiseRestartManager();
+
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", ["addon1@tests.mozilla.org"]);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ // user-disable two add-ons
+ let [a2, a4] = yield promiseAddonsByIDs(["min1max2@tests.mozilla.org",
+ "min1max3@tests.mozilla.org"]);
+ do_check_true(a2 != null && a4 != null);
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ yield promiseRestartManager();
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ let addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v1(addons);
+
+ // Restart as version 2, add-on _1 should become app-disabled
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("2");
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", ["min1max1@tests.mozilla.org"]);
+ check_startup_changes("enabled", []);
+ do_check_false(WindowWatcher.expected);
+
+ addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v2(addons);
+});
+
+// Upgrade to version 3 which will appDisable addons
+// upgradeable1x2-3 and override1x2-1x3
+// Only the newly disabled add-ons should be passed to the
+// upgrade window
+add_task(function* run_test_1() {
+ gCheckUpdates = true;
+ WindowWatcher.expected = true;
+
+ yield promiseRestartManager("3");
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", ["upgradeable1x2-3@tests.mozilla.org"]);
+ check_startup_changes("enabled", []);
+ do_check_false(WindowWatcher.expected);
+ gCheckUpdates = false;
+
+ let addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v3(addons);
+
+ do_check_eq(WindowWatcher.args.length, 2);
+ do_check_true(WindowWatcher.args.indexOf("upgradeable1x2-3@tests.mozilla.org") >= 0);
+ do_check_true(WindowWatcher.args.indexOf("override1x2-1x3@tests.mozilla.org") >= 0);
+});
+
+// Downgrade to version 2 which will remove appDisable from two add-ons
+// Still displays the compat window, because metadata is not recently updated
+add_task(function* run_test_2() {
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("2");
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", ["upgradeable1x2-3@tests.mozilla.org"]);
+ do_check_false(WindowWatcher.expected);
+
+ let addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v2(addons);
+});
+
+// Upgrade back to version 3 which should only appDisable
+// upgradeable1x2-3, because we already have the override
+// stored in our DB for override1x2-1x3. Ensure that when
+// the upgrade dialog updates an add-on no restart is necessary
+add_task(function* run_test_5() {
+ Services.prefs.setBoolPref(PREF_EM_SHOW_MISMATCH_UI, true);
+ // tell the mock compatibility window to install the available upgrade
+ gInstallUpdate = true;
+
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("3");
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", ["upgradeable1x2-3@tests.mozilla.org"]);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+ do_check_false(WindowWatcher.expected);
+ gInstallUpdate = false;
+
+ let addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v3_2(addons);
+
+ do_check_eq(WindowWatcher.args.length, 1);
+ do_check_true(WindowWatcher.args.indexOf("upgradeable1x2-3@tests.mozilla.org") >= 0);
+});
+
+// Downgrade to version 1 which will appEnable all the add-ons
+// except upgradeable1x2-3; the update we installed isn't compatible with 1
+add_task(function* run_test_6() {
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("1");
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", ["upgradeable1x2-3@tests.mozilla.org"]);
+ check_startup_changes("enabled", ["min1max1@tests.mozilla.org"]);
+ do_check_false(WindowWatcher.expected);
+
+ let addons = yield promiseAddonsByIDs(["min1max1@tests.mozilla.org",
+ "min1max2@tests.mozilla.org",
+ "upgradeable1x2-3@tests.mozilla.org",
+ "min1max3@tests.mozilla.org",
+ "min1max3b@tests.mozilla.org",
+ "override1x2-1x3@tests.mozilla.org"]);
+ check_state_v1_2(addons);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug554133.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug554133.js
new file mode 100644
index 000000000..c252e1ced
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug554133.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that if the AMO response provides total_results,
+// searchSucceeded is called with the correct number of total results
+
+Components.utils.import("resource://gre/modules/addons/AddonRepository.jsm");
+
+const PREF_GETADDONS_GETSEARCHRESULTS = "extensions.getAddons.search.url";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var server;
+
+var TESTS = [
+{
+ query: "bug554133",
+ maxResults: 2,
+ length: 2,
+ total: 100
+},
+{
+ query: "bug554133",
+ maxResults: 10,
+ length: 10,
+ total: 100
+},
+{
+ query: "bug554133",
+ maxResults: 100,
+ length: 10,
+ total: 100
+}
+];
+
+var gCurrentTest = 0;
+var SearchCallback = {
+ searchSucceeded: function(addons, length, total) {
+ do_check_false(AddonRepository.isSearching);
+ do_check_eq(addons.length, length);
+ do_check_eq(length, TESTS[gCurrentTest].length);
+ do_check_eq(total, TESTS[gCurrentTest].total);
+
+ gCurrentTest++;
+ run_current_test();
+ },
+
+ searchFailed: function() {
+ server.stop(do_test_finished);
+ do_throw("Search results failed");
+ }
+};
+
+function run_current_test() {
+ if (gCurrentTest < TESTS.length) {
+ var query = TESTS[gCurrentTest].query;
+ var maxResults = TESTS[gCurrentTest].maxResults;
+ AddonRepository.searchAddons(query, maxResults, SearchCallback);
+ }
+ else
+ server.stop(do_test_finished);
+}
+
+function run_test()
+{
+ // Setup for test
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ startupManager();
+
+ server = new HttpServer();
+ server.registerDirectory("/", do_get_file("data"));
+ mapFile("/data/test_bug554133.xml", server);
+ server.start(-1);
+ gPort = server.identity.primaryPort;
+
+ // Point search to the test server
+ Services.prefs.setCharPref(PREF_GETADDONS_GETSEARCHRESULTS,
+ "http://localhost:" + gPort + "/data/test_%TERMS%.xml");
+
+ do_check_neq(AddonRepository, null);
+ gCurrentTest = 0;
+ run_current_test();
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js
new file mode 100644
index 000000000..866ad3ad7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug559800.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that deleting the database from the profile doesn't break
+// anything
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// getting an unused port
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_update.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ startupManager();
+
+ do_test_pending();
+
+ run_test_1();
+}
+
+function end_test() {
+ gServer.stop(do_test_finished);
+}
+
+function run_test_1() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ shutdownManager();
+
+ gExtensionsJSON.remove(true);
+
+ do_execute_soon(check_test_1);
+ }));
+}
+
+function check_test_1() {
+ startupManager(false);
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ // due to delayed write, the file may not exist until
+ // after shutdown
+ shutdownManager();
+ do_check_true(gExtensionsJSON.exists());
+ do_check_true(gExtensionsJSON.fileSize > 0);
+
+ end_test();
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js
new file mode 100644
index 000000000..497e66526
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug563256.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the themes switch as expected
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension({
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Default",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "alternate@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ type: 4,
+ internalName: "alternate/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "alternate@tests.mozilla.org"], function([d, a]) {
+ do_check_neq(d, null);
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_neq(a, null);
+ do_check_true(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ run_test_1(d, a);
+ });
+}
+
+function end_test() {
+ do_execute_soon(do_test_finished);
+}
+
+// Checks switching to a different theme and back again leaves everything the
+// same
+function run_test_1(d, a) {
+ a.userDisabled = false;
+
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_false(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ d.userDisabled = false;
+
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_true(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ do_execute_soon(run_test_2);
+}
+
+// Tests that after the restart themes can be changed as expected
+function run_test_2() {
+ restartManager();
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "alternate@tests.mozilla.org"], function([d, a]) {
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ do_check_neq(d, null);
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_neq(a, null);
+ do_check_true(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ a.userDisabled = false;
+
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_false(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ d.userDisabled = false;
+
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_true(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ a.userDisabled = false;
+
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_false(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ do_execute_soon(check_test_2);
+ });
+}
+
+function check_test_2() {
+ restartManager();
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "alternate@tests.mozilla.org"], callback_soon(function([d, a]) {
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "alternate/1.0");
+
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_false(d.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_false(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_true(a.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ d.userDisabled = false;
+
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_false(d.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, d.id));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_true(a.userDisabled);
+ do_check_false(a.appDisabled);
+ do_check_true(a.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, a.id));
+ do_check_false(hasFlag(a.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "alternate/1.0");
+
+ restartManager();
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "alternate@tests.mozilla.org"], function([d2, a2]) {
+ do_check_neq(d2, null);
+ do_check_false(d2.userDisabled);
+ do_check_false(d2.appDisabled);
+ do_check_true(d2.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, d2.id));
+ do_check_false(hasFlag(d2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(d2.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_neq(a2, null);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ end_test();
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
new file mode 100644
index 000000000..92ba3d68f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug564030.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that upgrading an incompatible add-on to a compatible one forces an
+// EM restart
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9.2");
+
+ var dest = writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+ }, profileDir);
+ // Attempt to make this look like it was added some time in the past so
+ // the update makes the last modified time change.
+ setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
+
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a) {
+ do_check_neq(a, null);
+ do_check_eq(a.version, "1.0");
+ do_check_false(a.userDisabled);
+ do_check_true(a.appDisabled);
+ do_check_false(a.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a.id));
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_eq(a2.version, "2.0");
+ do_check_false(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug566626.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug566626.js
new file mode 100644
index 000000000..641ff87c9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug566626.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that multiple calls to the async API return fully formed
+// add-ons
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gAddon;
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Verifies that multiple calls to get an add-on at various stages of execution
+// return an add-on with a valid name.
+function run_test_1() {
+ var count = 0;
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.name, "Test 1");
+
+ if (count == 0)
+ gAddon = a1;
+ else
+ do_check_eq(a1, gAddon);
+ count++;
+ if (count == 4)
+ run_test_2();
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.name, "Test 1");
+
+ if (count == 0)
+ gAddon = a1;
+ else
+ do_check_eq(a1, gAddon);
+ count++;
+ if (count == 4)
+ run_test_2();
+ });
+
+ do_execute_soon(function() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.name, "Test 1");
+
+ if (count == 0)
+ gAddon = a1;
+ else
+ do_check_eq(a1, gAddon);
+ count++;
+ if (count == 4)
+ run_test_2();
+ });
+ });
+
+ do_execute_soon(function() {
+ do_execute_soon(function() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.name, "Test 1");
+
+ if (count == 0)
+ gAddon = a1;
+ else
+ do_check_eq(a1, gAddon);
+ count++;
+ if (count == 4)
+ run_test_2();
+ });
+ });
+ });
+}
+
+// Verifies that a subsequent call gets the same add-on from the cache
+function run_test_2() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.name, "Test 1");
+
+ do_check_eq(a1, gAddon);
+
+ do_execute_soon(do_test_finished);
+ });
+
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug567184.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug567184.js
new file mode 100644
index 000000000..0e7863068
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug567184.js
@@ -0,0 +1,53 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Tests that installing doesn't require a restart
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bug567184"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+
+ prepare_test({
+ "bug567184@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_1);
+ install.install();
+ });
+}
+
+function check_test_1() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID("bug567184@tests.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_true(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_false(b1.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug569138.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug569138.js
new file mode 100644
index 000000000..4869fc117
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug569138.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons with invalid target application entries show
+// up in the API but are correctly appDisabled
+
+// A working add-on
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Missing id
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+// Missing minVersion
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ maxVersion: "1"
+ }]
+};
+
+// Missing maxVersion
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1"
+ }]
+};
+
+// Blank id
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+// Blank minVersion
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "",
+ maxVersion: "1"
+ }]
+};
+
+// Blank maxVersion
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: ""
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Set up the profile
+function run_test() {
+ do_test_pending();
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.appDisabled);
+ do_check_false(a2.isActive);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+
+ do_check_neq(a4, null);
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.isActive);
+
+ do_check_neq(a5, null);
+ do_check_true(a5.appDisabled);
+ do_check_false(a5.isActive);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.appDisabled);
+ do_check_false(a6.isActive);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.appDisabled);
+ do_check_false(a6.isActive);
+
+ do_execute_soon(do_test_finished);
+
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug570173.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug570173.js
new file mode 100644
index 000000000..2c87d8c79
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug570173.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-on update check failures are propogated correctly
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+var testserver;
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Create and configure the HTTP server.
+ testserver = createHttpServer();
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+ gPort = testserver.identity.primaryPort;
+
+ run_next_test();
+}
+
+// Verify that an update check returns the correct errors.
+add_task(function* () {
+ for (let manifestType of ["rdf", "json"]) {
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: `http://localhost:${gPort}/data/test_missing.${manifestType}`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ bootstrap: "true",
+ }, profileDir);
+
+ yield promiseRestartManager();
+
+ let addon = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ ok(addon);
+ ok(addon.updateURL.endsWith(manifestType));
+ equal(addon.version, "1.0");
+
+ // We're expecting an error, so resolve when the promise is rejected.
+ let update = yield promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED)
+ .catch(Promise.resolve);
+
+ ok(!update.compatibilityUpdate, "not expecting a compatibility update");
+ ok(!update.updateAvailable, "not expecting a compatibility update");
+
+ equal(update.error, AddonManager.UPDATE_STATUS_DOWNLOAD_ERROR);
+
+ addon.uninstall();
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug576735.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug576735.js
new file mode 100644
index 000000000..df64e159d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug576735.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that we recover gracefully from an extension directory disappearing
+// when we were expecting to uninstall it.
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ a1.uninstall();
+
+ shutdownManager();
+
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+
+ writeInstallRDFForExtension(addon2, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"],
+ function([a1_2, a2_2]) {
+ // Addon1 should no longer be installed
+ do_check_eq(a1_2, null);
+
+ // Addon2 should have been detected
+ do_check_neq(a2_2, null);
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
new file mode 100644
index 000000000..8d9857e7f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug587088.js
@@ -0,0 +1,174 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that trying to upgrade or uninstall an extension that has a file locked
+// will roll back the upgrade or uninstall and retry at the next restart
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ // This is only an issue on windows.
+ if (!("nsIWindowsRegKey" in AM_Ci))
+ return;
+
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ run_test_1();
+}
+
+function check_addon(aAddon, aVersion) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, aVersion);
+ do_check_true(aAddon.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
+
+ do_check_true(aAddon.hasResource("testfile"));
+ if (aVersion == "1.0") {
+ do_check_true(aAddon.hasResource("testfile1"));
+ do_check_false(aAddon.hasResource("testfile2"));
+ }
+ else {
+ do_check_false(aAddon.hasResource("testfile1"));
+ do_check_true(aAddon.hasResource("testfile2"));
+ }
+
+ do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_NONE);
+}
+
+function check_addon_upgrading(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "1.0");
+ do_check_true(aAddon.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
+
+ do_check_true(aAddon.hasResource("testfile"));
+ do_check_true(aAddon.hasResource("testfile1"));
+ do_check_false(aAddon.hasResource("testfile2"));
+
+ do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_UPGRADE);
+
+ do_check_eq(aAddon.pendingUpgrade.version, "2.0");
+}
+
+function check_addon_uninstalling(aAddon, aAfterRestart) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "1.0");
+
+ if (aAfterRestart) {
+ do_check_false(aAddon.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, aAddon.id));
+ }
+ else {
+ do_check_true(aAddon.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, aAddon.id));
+ }
+
+ do_check_true(aAddon.hasResource("testfile"));
+ do_check_true(aAddon.hasResource("testfile1"));
+ do_check_false(aAddon.hasResource("testfile2"));
+
+ do_check_eq(aAddon.pendingOperations, AddonManager.PENDING_UNINSTALL);
+}
+
+function run_test_1() {
+ installAllFiles([do_get_addon("test_bug587088_1")], function() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ check_addon(a1, "1.0");
+
+ // Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
+ let uri = a1.getResourceURI("install.rdf");
+ if (uri.schemeIs("jar"))
+ uri = a1.getResourceURI();
+
+ let fstream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
+
+ installAllFiles([do_get_addon("test_bug587088_2")], function() {
+
+ check_addon_upgrading(a1);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_2) {
+ check_addon_upgrading(a1_2);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_3) {
+ check_addon_upgrading(a1_3);
+
+ fstream.close();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_4) {
+ check_addon(a1_4, "2.0");
+
+ a1_4.uninstall();
+ do_execute_soon(run_test_2);
+ });
+ }));
+ }));
+ });
+ });
+ });
+}
+
+// Test that a failed uninstall gets rolled back
+function run_test_2() {
+ restartManager();
+
+ installAllFiles([do_get_addon("test_bug587088_1")], function() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ check_addon(a1, "1.0");
+
+ // Lock either install.rdf for unpacked add-ons or the xpi for packed add-ons.
+ let uri = a1.getResourceURI("install.rdf");
+ if (uri.schemeIs("jar"))
+ uri = a1.getResourceURI();
+
+ let fstream = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ fstream.init(uri.QueryInterface(AM_Ci.nsIFileURL).file, -1, 0, 0);
+
+ a1.uninstall();
+
+ check_addon_uninstalling(a1);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_2) {
+ check_addon_uninstalling(a1_2, true);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_3) {
+ check_addon_uninstalling(a1_3, true);
+
+ fstream.close();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_4) {
+ do_check_eq(a1_4, null);
+ var dir = profileDir.clone();
+ dir.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ do_check_false(dir.exists());
+ do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+ }));
+ }));
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js
new file mode 100644
index 000000000..858579815
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug594058.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests is modifying a file in an unpacked extension
+// causes cache invalidation.
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// Allow the mismatch UI to show
+Services.prefs.setBoolPref("extensions.showMismatchUI", true);
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+var Ci = Components.interfaces;
+const extDir = gProfD.clone();
+extDir.append("extensions");
+
+var gCachePurged = false;
+
+// Override the window watcher
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, args) {
+ do_check_false(gCachePurged);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+/**
+ * Start the test by installing extensions.
+ */
+function run_test() {
+ do_test_pending();
+ gCachePurged = false;
+
+ let obs = AM_Cc["@mozilla.org/observer-service;1"].
+ getService(AM_Ci.nsIObserverService);
+ obs.addObserver({
+ observe: function(aSubject, aTopic, aData) {
+ gCachePurged = true;
+ }
+ }, "startupcache-invalidate", false);
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ startupManager();
+ // nsAppRunner takes care of clearing this when a new app is installed
+ do_check_false(gCachePurged);
+
+ installAllFiles([do_get_addon("test_bug594058")], function() {
+ restartManager();
+ do_check_true(gCachePurged);
+ gCachePurged = false;
+
+ // Now, make it look like we've updated the file. First, start the EM
+ // so it records the bogus old time, then update the file and restart.
+ let extFile = extDir.clone();
+ let pastTime = extFile.lastModifiedTime - 5000;
+ extFile.append("bug594058@tests.mozilla.org");
+ setExtensionModifiedTime(extFile, pastTime);
+ let otherFile = extFile.clone();
+ otherFile.append("directory");
+ otherFile.lastModifiedTime = pastTime;
+ otherFile.append("file1");
+ otherFile.lastModifiedTime = pastTime;
+
+ restartManager();
+ gCachePurged = false;
+
+ otherFile.lastModifiedTime = pastTime + 5000;
+ restartManager();
+ do_check_true(gCachePurged);
+ gCachePurged = false;
+
+ restartManager();
+ do_check_false(gCachePurged);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug595081.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug595081.js
new file mode 100644
index 000000000..db53dc747
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug595081.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that the AddonManager objects cannot be tampered with
+
+function run_test() {
+ // Setup for test
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+
+ // Verify that properties cannot be changed
+ let old = AddonManager.STATE_AVAILABLE;
+ AddonManager.STATE_AVAILABLE = 28;
+ do_check_eq(AddonManager.STATE_AVAILABLE, old);
+
+ // Verify that functions cannot be replaced
+ AddonManager.isInstallEnabled = function() {
+ do_throw("Should not be able to replace a function");
+ }
+ AddonManager.isInstallEnabled("application/x-xpinstall");
+
+ // Verify that properties cannot be added
+ AddonManager.foo = "bar";
+ do_check_false("foo" in AddonManager);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js
new file mode 100644
index 000000000..7e2bf7d77
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug595573.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests if addons with UUID based ids install and stay installed
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ run_test_1();
+}
+
+function run_test_1() {
+ installAllFiles([do_get_addon("test_bug595573")], function() {
+ restartManager();
+
+ AddonManager.getAddonByID("{2f69dacd-03df-4150-a9f1-e8a7b2748829}", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_execute_soon(run_test_2);
+ });
+ });
+}
+
+function run_test_2() {
+ restartManager();
+
+ AddonManager.getAddonByID("{2f69dacd-03df-4150-a9f1-e8a7b2748829}", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug596607.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug596607.js
new file mode 100644
index 000000000..bdcf93a1f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug596607.js
@@ -0,0 +1,147 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that a reference to a non-existent extension in the registry doesn't
+// break things
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +
+ AddonManager.SCOPE_SYSTEM);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+const addon1Dir = writeInstallRDFForExtension(addon1, gProfD, "addon1");
+const addon2Dir = writeInstallRDFForExtension(addon2, gProfD, "addon2");
+const addon3Dir = gProfD.clone();
+addon3Dir.append("addon3@tests.mozilla.org");
+
+let registry;
+
+function run_test() {
+ // This test only works where there is a registry.
+ if (!("nsIWindowsRegKey" in AM_Ci))
+ return;
+
+ registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+
+ do_test_pending();
+
+ run_test_1();
+}
+
+// Tests whether starting a fresh profile with a bad entry works
+function run_test_1() {
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", addon1Dir.path);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", addon2Dir.path);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon3@tests.mozilla.org", addon3Dir.path);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"], function([a1, a2, a3]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a1.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_check_eq(a3, null);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Tests whether removing the bad entry has any effect
+function run_test_2() {
+ shutdownManager();
+
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon3@tests.mozilla.org", addon3Dir.path);
+
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"], function([a1, a2, a3]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a1.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_check_eq(a3, null);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Tests adding the bad entry to an existing profile has any effect
+function run_test_3() {
+ shutdownManager();
+
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon3@tests.mozilla.org", null);
+
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"], function([a1, a2, a3]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a1.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_check_eq(a3, null);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js
new file mode 100644
index 000000000..d0c973960
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug616841.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that string comparisons work correctly in callbacks
+
+function test_string_compare() {
+ do_check_true("C".localeCompare("D") < 0);
+ do_check_true("D".localeCompare("C") > 0);
+ do_check_true("\u010C".localeCompare("D") < 0);
+ do_check_true("D".localeCompare("\u010C") > 0);
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ do_test_pending();
+
+ test_string_compare();
+
+ AddonManager.getAddonByID("foo", function(aAddon) {
+ test_string_compare();
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug619730.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug619730.js
new file mode 100644
index 000000000..1c21385e0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug619730.js
@@ -0,0 +1,64 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests whether
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_bug619730.xml", gTestserver);
+
+function load_blocklist(file, aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ do_execute_soon(aCallback);
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+var gSawGFX = false;
+var gSawTest = false;
+
+// Performs the initial setup
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ do_check_true(aSubject instanceof AM_Ci.nsIDOMElement);
+ do_check_eq(aSubject.getAttribute("testattr"), "GFX");
+ do_check_eq(aSubject.childNodes.length, 2);
+ gSawGFX = true;
+ }, "blocklist-data-gfxItems", false);
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ do_check_true(aSubject instanceof AM_Ci.nsIDOMElement);
+ do_check_eq(aSubject.getAttribute("testattr"), "FOO");
+ do_check_eq(aSubject.childNodes.length, 3);
+ gSawTest = true;
+ }, "blocklist-data-testItems", false);
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ do_check_true(gSawGFX);
+ do_check_true(gSawTest);
+ }, "blocklist-data-fooItems", false);
+
+ // Need to wait for the blocklist to load; Bad Things happen if the test harness
+ // shuts down AddonManager before the blocklist service is done telling it about
+ // changes
+ load_blocklist("test_bug619730.xml", () => gTestserver.stop(do_test_finished));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug620837.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug620837.js
new file mode 100644
index 000000000..6bfbfcaf2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug620837.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/httpd.js");
+
+const PREF_BLOCKLIST_LASTUPDATETIME = "app.update.lastUpdateTime.blocklist-background-update-timer";
+const PREF_BLOCKLIST_PINGCOUNTTOTAL = "extensions.blocklist.pingCountTotal";
+const PREF_BLOCKLIST_PINGCOUNTVERSION = "extensions.blocklist.pingCountVersion";
+
+const SECONDS_IN_DAY = 60 * 60 * 24;
+
+var gExpectedQueryString = null;
+var gNextTest = null;
+var gTestserver = null;
+
+function notify_blocklist() {
+ var blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+function pathHandler(metadata, response) {
+ do_check_eq(metadata.queryString, gExpectedQueryString);
+ gNextTest();
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ gTestserver = new HttpServer();
+ gTestserver.registerPathHandler("/", pathHandler);
+ gTestserver.start(-1);
+ gPort = gTestserver.identity.primaryPort;
+
+ Services.prefs.setCharPref("extensions.blocklist.url",
+ "http://localhost:" + gPort +
+ "/?%PING_COUNT%&%TOTAL_PING_COUNT%&%DAYS_SINCE_LAST_PING%");
+
+ do_test_pending();
+ test1();
+}
+
+function getNowInSeconds() {
+ return Math.round(Date.now() / 1000);
+}
+
+function test1() {
+ gNextTest = test2;
+ gExpectedQueryString = "1&1&new";
+ notify_blocklist();
+}
+
+function test2() {
+ gNextTest = test3;
+ gExpectedQueryString = "invalid&invalid&invalid";
+ notify_blocklist();
+}
+
+function test3() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - SECONDS_IN_DAY));
+ gNextTest = test4;
+ gExpectedQueryString = "2&2&1";
+ notify_blocklist();
+}
+
+function test4() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, -1);
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 2)));
+ gNextTest = test5;
+ gExpectedQueryString = "1&3&2";
+ notify_blocklist();
+}
+
+function test5() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME, getNowInSeconds());
+ gNextTest = test6;
+ gExpectedQueryString = "invalid&invalid&0";
+ notify_blocklist();
+}
+
+function test6() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 3)));
+ gNextTest = test7;
+ gExpectedQueryString = "2&4&3";
+ notify_blocklist();
+}
+
+function test7() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, 2147483647);
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 4)));
+ gNextTest = test8;
+ gExpectedQueryString = "2147483647&5&4";
+ notify_blocklist();
+}
+
+function test8() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 5)));
+ gNextTest = test9;
+ gExpectedQueryString = "1&6&5";
+ notify_blocklist();
+}
+
+function test9() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTTOTAL, 2147483647);
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 6)));
+ gNextTest = test10;
+ gExpectedQueryString = "2&2147483647&6";
+ notify_blocklist();
+}
+
+function test10() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 7)));
+ gNextTest = test11;
+ gExpectedQueryString = "3&1&7";
+ notify_blocklist();
+}
+
+function test11() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_PINGCOUNTVERSION, -1);
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 8)));
+ gNextTest = test12;
+ gExpectedQueryString = "1&2&8";
+ notify_blocklist();
+}
+
+function test12() {
+ Services.prefs.setIntPref(PREF_BLOCKLIST_LASTUPDATETIME,
+ (getNowInSeconds() - (SECONDS_IN_DAY * 9)));
+ gNextTest = finish;
+ gExpectedQueryString = "2&3&9";
+ notify_blocklist();
+}
+
+function finish() {
+ gTestserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js
new file mode 100644
index 000000000..449c59065
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug655254.js
@@ -0,0 +1,164 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that moving an extension in the filesystem without any other
+// change still keeps updated compatibility information
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9.2");
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_bug655254.rdf", testserver);
+
+var userDir = gProfD.clone();
+userDir.append("extensions2");
+userDir.append(gAppInfo.ID);
+
+var dirProvider = {
+ getFile: function(aProp, aPersistent) {
+ aPersistent.value = false;
+ if (aProp == "XREUSysExt")
+ return userDir.parent;
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIDirectoryServiceProvider,
+ AM_Ci.nsISupports])
+};
+Services.dirsvc.registerProvider(dirProvider);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ updateURL: "http://localhost:" + gPort + "/data/test_bug655254.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Set up the profile
+function run_test() {
+ do_test_pending();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test_1() {
+ var time = Date.now();
+ var dir = writeInstallRDFForExtension(addon1, userDir);
+ setExtensionModifiedTime(dir, time);
+
+ manuallyInstall(do_get_addon("test_bug655254_2"), userDir, "addon2@tests.mozilla.org");
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.appDisabled);
+ do_check_false(a1.isActive);
+ do_check_false(isExtensionInAddonsList(userDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isActive);
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 1);
+
+ a1.findUpdates({
+ onUpdateFinished: function() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1_2) {
+ do_check_neq(a1_2, null);
+ do_check_false(a1_2.appDisabled);
+ do_check_true(a1_2.isActive);
+ do_check_true(isExtensionInAddonsList(userDir, a1_2.id));
+
+ shutdownManager();
+
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 0);
+
+ userDir.parent.moveTo(gProfD, "extensions3");
+ userDir = gProfD.clone();
+ userDir.append("extensions3");
+ userDir.append(gAppInfo.ID);
+ do_check_true(userDir.exists());
+
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1_3, a2_3]) {
+ do_check_neq(a1_3, null);
+ do_check_false(a1_3.appDisabled);
+ do_check_true(a1_3.isActive);
+ do_check_true(isExtensionInAddonsList(userDir, a1_3.id));
+
+ do_check_neq(a2_3, null);
+ do_check_false(a2_3.appDisabled);
+ do_check_true(a2_3.isActive);
+ do_check_false(isExtensionInAddonsList(userDir, a2_3.id));
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 1);
+
+ do_execute_soon(run_test_2);
+ });
+ }));
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+// Set up the profile
+function run_test_2() {
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isActive);
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 1);
+
+ a2.userDisabled = true;
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 0);
+
+ shutdownManager();
+
+ userDir.parent.moveTo(gProfD, "extensions4");
+ userDir = gProfD.clone();
+ userDir.append("extensions4");
+ userDir.append(gAppInfo.ID);
+ do_check_true(userDir.exists());
+
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1_2, a2_2]) {
+ do_check_neq(a1_2, null);
+ do_check_false(a1_2.appDisabled);
+ do_check_true(a1_2.isActive);
+ do_check_true(isExtensionInAddonsList(userDir, a1_2.id));
+
+ do_check_neq(a2_2, null);
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.isActive);
+ do_check_false(isExtensionInAddonsList(userDir, a2_2.id));
+ do_check_eq(Services.prefs.getIntPref("bootstraptest.active_version"), 0);
+
+ end_test();
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js
new file mode 100644
index 000000000..6e98a69a4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug659772.js
@@ -0,0 +1,340 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that a pending upgrade during a schema update doesn't break things
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ run_test_1();
+}
+
+// Tests whether a schema migration without app version change works
+function run_test_1() {
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_false(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon1.id));
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.version, "2.0");
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon2.id));
+
+ do_check_neq(a3, null);
+ do_check_eq(a3.version, "2.0");
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon3.id));
+
+ do_check_neq(a4, null);
+ do_check_eq(a4.version, "2.0");
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, addon4.id));
+
+ // Prepare the add-on update, and a bootstrapped addon (bug 693714)
+ installAllFiles([
+ do_get_addon("test_bug659772"),
+ do_get_addon("test_bootstrap1_1")
+ ], function() {
+ shutdownManager();
+
+ // Make it look like the next time the app is started it has a new DB schema
+ changeXPIDBVersion(1);
+ Services.prefs.setIntPref("extensions.databaseSchema", 1);
+
+ let jsonfile = gProfD.clone();
+ jsonfile.append("extensions");
+ jsonfile.append("staged");
+ jsonfile.append("addon3@tests.mozilla.org.json");
+ do_check_true(jsonfile.exists());
+
+ // Remove an unnecessary property from the cached manifest
+ let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ let json = AM_Cc["@mozilla.org/dom/json;1"].
+ createInstance(AM_Ci.nsIJSON);
+ fis.init(jsonfile, -1, 0, 0);
+ let addonObj = json.decodeFromStream(fis, jsonfile.fileSize);
+ fis.close();
+ delete addonObj.optionsType;
+
+ let stream = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(AM_Ci.nsIFileOutputStream);
+ let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(AM_Ci.nsIConverterOutputStream);
+ stream.init(jsonfile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(JSON.stringify(addonObj));
+ converter.close();
+ stream.close();
+
+ Services.prefs.clearUserPref("bootstraptest.install_reason");
+ Services.prefs.clearUserPref("bootstraptest.uninstall_reason");
+
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1_2, a2_2, a3_2, a4_2]) {
+ do_check_neq(a1_2, null);
+ do_check_eq(a1_2.version, "2.0");
+ do_check_false(a1_2.appDisabled);
+ do_check_false(a1_2.userDisabled);
+ do_check_true(a1_2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon1.id));
+
+ do_check_neq(a2_2, null);
+ do_check_eq(a2_2.version, "2.0");
+ do_check_false(a2_2.appDisabled);
+ do_check_false(a2_2.userDisabled);
+ do_check_true(a2_2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon2.id));
+
+ // Should stay enabled because we migrate the compat info from
+ // the previous version of the DB
+ do_check_neq(a3_2, null);
+ do_check_eq(a3_2.version, "2.0");
+ todo_check_false(a3_2.appDisabled); // XXX unresolved issue
+ do_check_false(a3_2.userDisabled);
+ todo_check_true(a3_2.isActive); // XXX same
+ todo_check_true(isExtensionInAddonsList(profileDir, addon3.id)); // XXX same
+
+ do_check_neq(a4_2, null);
+ do_check_eq(a4_2.version, "2.0");
+ do_check_true(a4_2.appDisabled);
+ do_check_false(a4_2.userDisabled);
+ do_check_false(a4_2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, addon4.id));
+
+ // Check that install and uninstall haven't been called on the bootstrapped addon
+ do_check_false(Services.prefs.prefHasUserValue("bootstraptest.install_reason"));
+ do_check_false(Services.prefs.prefHasUserValue("bootstraptest.uninstall_reason"));
+
+ a1_2.uninstall();
+ a2_2.uninstall();
+ a3_2.uninstall();
+ a4_2.uninstall();
+ do_execute_soon(run_test_2);
+ });
+ });
+ });
+}
+
+// Tests whether a schema migration with app version change works
+function run_test_2() {
+ restartManager();
+
+ shutdownManager();
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_false(a1.appDisabled);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon1.id));
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.version, "2.0");
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon2.id));
+
+ do_check_neq(a3, null);
+ do_check_eq(a3.version, "2.0");
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon3.id));
+
+ do_check_neq(a4, null);
+ do_check_eq(a4.version, "2.0");
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, addon4.id));
+
+ // Prepare the add-on update, and a bootstrapped addon (bug 693714)
+ installAllFiles([
+ do_get_addon("test_bug659772"),
+ do_get_addon("test_bootstrap1_1")
+ ], function() { do_execute_soon(prepare_schema_migrate); });
+
+ function prepare_schema_migrate() {
+ shutdownManager();
+
+ // Make it look like the next time the app is started it has a new DB schema
+ changeXPIDBVersion(1);
+ Services.prefs.setIntPref("extensions.databaseSchema", 1);
+
+ let jsonfile = gProfD.clone();
+ jsonfile.append("extensions");
+ jsonfile.append("staged");
+ jsonfile.append("addon3@tests.mozilla.org.json");
+ do_check_true(jsonfile.exists());
+
+ // Remove an unnecessary property from the cached manifest
+ let fis = AM_Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(AM_Ci.nsIFileInputStream);
+ let json = AM_Cc["@mozilla.org/dom/json;1"].
+ createInstance(AM_Ci.nsIJSON);
+ fis.init(jsonfile, -1, 0, 0);
+ let addonObj = json.decodeFromStream(fis, jsonfile.fileSize);
+ fis.close();
+ delete addonObj.optionsType;
+
+ let stream = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(AM_Ci.nsIFileOutputStream);
+ let converter = AM_Cc["@mozilla.org/intl/converter-output-stream;1"].
+ createInstance(AM_Ci.nsIConverterOutputStream);
+ stream.init(jsonfile, FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE, FileUtils.PERMS_FILE,
+ 0);
+ converter.init(stream, "UTF-8", 0, 0x0000);
+ converter.writeString(JSON.stringify(addonObj));
+ converter.close();
+ stream.close();
+
+ Services.prefs.clearUserPref("bootstraptest.install_reason");
+ Services.prefs.clearUserPref("bootstraptest.uninstall_reason");
+
+ gAppInfo.version = "2";
+ startupManager(true);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ callback_soon(function([a1_2, a2_2, a3_2, a4_2]) {
+ do_check_neq(a1_2, null);
+ do_check_eq(a1_2.version, "2.0");
+ do_check_true(a1_2.appDisabled);
+ do_check_false(a1_2.userDisabled);
+ do_check_false(a1_2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, addon1.id));
+
+ do_check_neq(a2_2, null);
+ do_check_eq(a2_2.version, "2.0");
+ do_check_false(a2_2.appDisabled);
+ do_check_false(a2_2.userDisabled);
+ do_check_true(a2_2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon2.id));
+
+ // Should become appDisabled because we migrate the compat info from
+ // the previous version of the DB
+ do_check_neq(a3_2, null);
+ do_check_eq(a3_2.version, "2.0");
+ todo_check_true(a3_2.appDisabled);
+ do_check_false(a3_2.userDisabled);
+ todo_check_false(a3_2.isActive);
+ todo_check_false(isExtensionInAddonsList(profileDir, addon3.id));
+
+ do_check_neq(a4_2, null);
+ do_check_eq(a4_2.version, "2.0");
+ do_check_false(a4_2.appDisabled);
+ do_check_false(a4_2.userDisabled);
+ do_check_true(a4_2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, addon4.id));
+
+ // Check that install and uninstall haven't been called on the bootstrapped addon
+ do_check_false(Services.prefs.prefHasUserValue("bootstraptest.install_reason"));
+ do_check_false(Services.prefs.prefHasUserValue("bootstraptest.uninstall_reason"));
+
+ a1_2.uninstall();
+ a2_2.uninstall();
+ a3_2.uninstall();
+ a4_2.uninstall();
+ restartManager();
+
+ shutdownManager();
+
+ do_test_finished();
+ }));
+ }
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug675371.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug675371.js
new file mode 100644
index 000000000..6f2a5e7cd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug675371.js
@@ -0,0 +1,91 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bug675371"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+
+ prepare_test({
+ "bug675371@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test));
+ install.install();
+ });
+}
+
+function check_test() {
+ AddonManager.getAddonByID("bug675371@tests.mozilla.org", do_exception_wrap(function(addon) {
+ do_check_neq(addon, null);
+ do_check_true(addon.isActive);
+
+ // Tests that chrome.manifest is registered when the addon is installed.
+ var target = { };
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_check_true(target.active);
+
+ prepare_test({
+ "bug675371@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ // Tests that chrome.manifest is unregistered when the addon is disabled.
+ addon.userDisabled = true;
+ target.active = false;
+ try {
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_throw("Chrome file should not have been found");
+ } catch (e) {
+ do_check_false(target.active);
+ }
+
+ prepare_test({
+ "bug675371@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ // Tests that chrome.manifest is registered when the addon is enabled.
+ addon.userDisabled = false;
+ target.active = false;
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_check_true(target.active);
+
+ prepare_test({
+ "bug675371@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ // Tests that chrome.manifest is unregistered when the addon is uninstalled.
+ addon.uninstall();
+ target.active = false;
+ try {
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_throw("Chrome file should not have been found");
+ } catch (e) {
+ do_check_false(target.active);
+ }
+
+ do_execute_soon(do_test_finished);
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug740612.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug740612.js
new file mode 100644
index 000000000..d17e7acde
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug740612.js
@@ -0,0 +1,40 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that attempts to override the global values fails but doesn't
+// destroy the world with it
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function getActiveVersion() {
+ return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function getInstalledVersion() {
+ return Services.prefs.getIntPref("bootstraptest.installed_version");
+}
+
+function run_test() {
+ do_test_pending();
+
+ manuallyInstall(do_get_addon("test_bug740612_1"), profileDir,
+ "bug740612_1@tests.mozilla.org");
+ manuallyInstall(do_get_addon("test_bug740612_2"), profileDir,
+ "bug740612_2@tests.mozilla.org");
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["bug740612_1@tests.mozilla.org",
+ "bug740612_2@tests.mozilla.org"],
+ function([a1, a2]) {
+ do_check_neq(a1, null);
+ do_check_neq(a2, null);
+ do_check_eq(getInstalledVersion(), "1.0");
+ do_check_eq(getActiveVersion(), "1.0");
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug753900.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug753900.js
new file mode 100644
index 000000000..206862339
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug753900.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that strange characters in an add-on version don't break the
+// crash annotation.
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1,0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1:0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1,0",
+ name: "Test 3",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1:0",
+ name: "Test 4",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+ do_check_neq(a2, null);
+ do_check_in_crash_annotation(addon2.id, addon2.version);
+ do_check_neq(a3, null);
+ do_check_in_crash_annotation(addon3.id, addon3.version);
+ do_check_neq(a4, null);
+ do_check_in_crash_annotation(addon4.id, addon4.version);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug757663.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug757663.js
new file mode 100644
index 000000000..54cee0839
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug757663.js
@@ -0,0 +1,112 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test verifies that removing a listener during a callback for that type
+// of listener still results in all listeners being called.
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 1",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var listener1 = {
+ sawEvent: false,
+ onDisabling: function() {
+ this.sawEvent = true;
+ AddonManager.removeAddonListener(this);
+ },
+ onNewInstall: function() {
+ this.sawEvent = true;
+ AddonManager.removeInstallListener(this);
+ }
+};
+var listener2 = {
+ sawEvent: false,
+ onDisabling: function() {
+ this.sawEvent = true;
+ },
+ onNewInstall: function() {
+ this.sawEvent = true;
+ }
+};
+var listener3 = {
+ sawEvent: false,
+ onDisabling: function() {
+ this.sawEvent = true;
+ },
+ onNewInstall: function() {
+ this.sawEvent = true;
+ }
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ startupManager();
+
+ run_test_1();
+}
+
+function run_test_1() {
+ AddonManager.addAddonListener(listener1);
+ AddonManager.addAddonListener(listener2);
+ AddonManager.addAddonListener(listener3);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org"], function([a1]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.isActive);
+
+ a1.userDisabled = true;
+
+ do_check_true(listener1.sawEvent);
+ listener1.sawEvent = false;
+ do_check_true(listener2.sawEvent);
+ listener2.sawEvent = false;
+ do_check_true(listener3.sawEvent);
+ listener3.sawEvent = false;
+
+ AddonManager.removeAddonListener(listener1);
+ AddonManager.removeAddonListener(listener2);
+ AddonManager.removeAddonListener(listener3);
+
+ a1.uninstall();
+ run_test_2();
+ });
+}
+
+function run_test_2() {
+ AddonManager.addInstallListener(listener1);
+ AddonManager.addInstallListener(listener2);
+ AddonManager.addInstallListener(listener3);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bug757663"), function(aInstall) {
+
+ do_check_true(listener1.sawEvent);
+ listener1.sawEvent = false;
+ do_check_true(listener2.sawEvent);
+ listener2.sawEvent = false;
+ do_check_true(listener3.sawEvent);
+ listener3.sawEvent = false;
+
+ AddonManager.removeInstallListener(listener1);
+ AddonManager.removeInstallListener(listener2);
+ AddonManager.removeInstallListener(listener3);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_bug953156.js b/toolkit/mozapps/extensions/test/xpcshell/test_bug953156.js
new file mode 100644
index 000000000..a7acb9ad2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_bug953156.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bug675371"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+
+ prepare_test({
+ "bug675371@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded"
+ ], callback_soon(check_test));
+ install.install();
+ });
+}
+
+function check_test() {
+ AddonManager.getAddonByID("bug675371@tests.mozilla.org", do_exception_wrap(function(addon) {
+ do_check_neq(addon, null);
+ do_check_true(addon.isActive);
+
+ // Tests that chrome.manifest is registered when the addon is installed.
+ var target = { active: false };
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_check_true(target.active);
+
+ shutdownManager();
+
+ // Tests that chrome.manifest remains registered at app shutdown.
+ target.active = false;
+ Services.scriptloader.loadSubScript("chrome://bug675371/content/test.js", target);
+ do_check_true(target.active);
+
+ do_execute_soon(do_test_finished);
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js b/toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js
new file mode 100644
index 000000000..edb442aad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_cache_certdb.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// We require signature checks for this test
+Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+gUseRealCertChecks = true;
+
+const CERT = `MIIDITCCAgmgAwIBAgIJALAv8fydd6nBMA0GCSqGSIb3DQEBBQUAMCcxJTAjBgNV
+BAMMHGJvb3RzdHJhcDFAdGVzdHMubW96aWxsYS5vcmcwHhcNMTYwMjAyMjMxNjUy
+WhcNMjYwMTMwMjMxNjUyWjAnMSUwIwYDVQQDDBxib290c3RyYXAxQHRlc3RzLm1v
+emlsbGEub3JnMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5caNuLTu
+H8dEqNntLlhKi4y09hrgcF3cb6n5Xx9DIHA8CKiZxt9qGXKeeiDwEiiQ8ibJYzdc
+jLkbzJUyPVUaH9ygrWynSpSTOvv/Ys3+ERrCo9W7Zuzwdmzt6TTEjFMS4lVx06us
+3uUqkdp3JMgCqCEbOFZiztICiSKrp8QFJkAfApZzBqmJOPOWH0yZ2CRRzvbQZ6af
+hqQDUalJQjWfsenyUWphhbREqExetxHJFR3OrmJt/shXVyz6dD7TBuE3PPUh1RpE
+3ejVufcTzjV3XmK79PxsKLM9V2+ww9e9V3OET57kyvn+bpSWdUYm3X4DA8dxNW6+
+kTFWRnQNZ+zQVQIDAQABo1AwTjAdBgNVHQ4EFgQUac36ccv+99N5HxYa8dCDYRaF
+HNQwHwYDVR0jBBgwFoAUac36ccv+99N5HxYa8dCDYRaFHNQwDAYDVR0TBAUwAwEB
+/zANBgkqhkiG9w0BAQUFAAOCAQEAFfu3MN8EtY5wcxOFdGShOmGQPm2MJJVE6MG+
+p4RqHrukHZSgKOyWjkRk7t6NXzNcnHco9HFv7FQRAXSJ5zObmyu+TMZlu4jHHCav
+GMcV3C/4SUGtlipZbgNe00UAIm6tM3Wh8dr38W7VYg4KGAwXou5XhQ9gCAnSn90o
+H/42NqHTjJsR4v18izX2aO25ARQdMby7Lsr5j9RqweHywiSlPusFcKRseqOnIP0d
+JT3+qh78LeMbNBO2mYD3SP/zu0TAmkAVNcj2KPw0+a0kVZ15rvslPC/K3xn9msMk
+fQthv3rDAcsWvi9YO7T+vylgZBgJfn1ZqpQqy58xN96uh6nPOw==`;
+
+function overrideCertDB() {
+ // Unregister the real database.
+ let registrar = Components.manager.QueryInterface(AM_Ci.nsIComponentRegistrar);
+ let factory = registrar.getClassObject(CERTDB_CID, AM_Ci.nsIFactory);
+ registrar.unregisterFactory(CERTDB_CID, factory);
+
+ // Get the real DB
+ let realCertDB = factory.createInstance(null, AM_Ci.nsIX509CertDB);
+
+ let fakeCert = realCertDB.constructX509FromBase64(CERT.replace(/\n/g, ""));
+
+ let fakeCertDB = {
+ openSignedAppFileAsync(root, file, callback) {
+ callback.openSignedAppFileFinished(Components.results.NS_OK, null, fakeCert);
+ },
+
+ verifySignedDirectoryAsync(root, dir, callback) {
+ callback.verifySignedDirectoryFinished(Components.results.NS_OK, fakeCert);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIX509CertDB])
+ };
+
+ for (let property of Object.keys(realCertDB)) {
+ if (property in fakeCertDB) {
+ continue;
+ }
+
+ if (typeof realCertDB[property] == "function") {
+ fakeCertDB[property] = realCertDB[property].bind(realCertDB);
+ }
+ }
+
+ let certDBFactory = {
+ createInstance: function(outer, iid) {
+ if (outer != null) {
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ }
+ return fakeCertDB.QueryInterface(iid);
+ }
+ };
+ registrar.registerFactory(CERTDB_CID, "CertDB",
+ CERTDB_CONTRACTID, certDBFactory);
+}
+
+add_task(function*() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ // Once the application is started we shouldn't be able to replace the
+ // certificate database
+ overrideCertDB();
+
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js b/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js
new file mode 100644
index 000000000..f3448abd2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_cacheflush.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that flushing the zipreader cache happens when appropriate
+
+var gExpectedFile = null;
+var gCacheFlushCount = 0;
+
+var CacheFlushObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "flush-cache-entry")
+ return;
+ // Ignore flushes triggered by the fake cert DB
+ if (aData == "cert-override")
+ return;
+
+ do_check_true(gExpectedFile != null);
+ do_check_true(aSubject instanceof AM_Ci.nsIFile);
+ do_check_eq(aSubject.path, gExpectedFile.path);
+ gCacheFlushCount++;
+ }
+};
+
+function run_test() {
+ do_test_pending();
+ Services.obs.addObserver(CacheFlushObserver, "flush-cache-entry", false);
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2");
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Tests that the cache is flushed when cancelling a pending install
+function run_test_1() {
+ AddonManager.getInstallForFile(do_get_addon("test_cacheflush1"), function(aInstall) {
+ completeAllInstalls([aInstall], function() {
+ // We should flush the staged XPI when cancelling the install
+ gExpectedFile = gProfD.clone();
+ gExpectedFile.append("extensions");
+ gExpectedFile.append("staged");
+ gExpectedFile.append("addon1@tests.mozilla.org.xpi");
+ aInstall.cancel();
+
+ do_check_eq(gCacheFlushCount, 1);
+ gExpectedFile = null;
+ gCacheFlushCount = 0;
+
+ run_test_2();
+ });
+ });
+}
+
+// Tests that the cache is flushed when uninstalling an add-on
+function run_test_2() {
+ installAllFiles([do_get_addon("test_cacheflush1")], function() {
+ // Installing will flush the staged XPI during startup
+ gExpectedFile = gProfD.clone();
+ gExpectedFile.append("extensions");
+ gExpectedFile.append("staged");
+ gExpectedFile.append("addon1@tests.mozilla.org.xpi");
+ restartManager();
+ do_check_eq(gCacheFlushCount, 1);
+ gExpectedFile = null;
+ gCacheFlushCount = 0;
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ // We should flush the installed XPI when uninstalling
+ do_check_true(a1 != null);
+ a1.uninstall();
+ do_check_eq(gCacheFlushCount, 0);
+
+ gExpectedFile = gProfD.clone();
+ gExpectedFile.append("extensions");
+ gExpectedFile.append("addon1@tests.mozilla.org.xpi");
+ restartManager();
+ do_check_eq(gCacheFlushCount, 1);
+ gExpectedFile = null;
+ gCacheFlushCount = 0;
+
+ do_execute_soon(run_test_3);
+ });
+ });
+}
+
+// Tests that the cache is flushed when installing a restartless add-on
+function run_test_3() {
+ AddonManager.getInstallForFile(do_get_addon("test_cacheflush2"), function(aInstall) {
+ aInstall.addListener({
+ onInstallStarted: function() {
+ // We should flush the staged XPI when completing the install
+ gExpectedFile = gProfD.clone();
+ gExpectedFile.append("extensions");
+ gExpectedFile.append("staged");
+ gExpectedFile.append("addon2@tests.mozilla.org.xpi");
+ },
+
+ onInstallEnded: function() {
+ do_check_eq(gCacheFlushCount, 1);
+ gExpectedFile = null;
+ gCacheFlushCount = 0;
+
+ do_execute_soon(run_test_4);
+ }
+ });
+
+ aInstall.install();
+ });
+}
+
+// Tests that the cache is flushed when uninstalling a restartless add-on
+function run_test_4() {
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ // We should flush the installed XPI when uninstalling
+ gExpectedFile = gProfD.clone();
+ gExpectedFile.append("extensions");
+ gExpectedFile.append("addon2@tests.mozilla.org.xpi");
+
+ a2.uninstall();
+ do_check_eq(gCacheFlushCount, 2);
+ gExpectedFile = null;
+ gCacheFlushCount = 0;
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_checkCompatibility_themeOverride.js b/toolkit/mozapps/extensions/test/xpcshell/test_checkCompatibility_themeOverride.js
new file mode 100644
index 000000000..b6cb13e08
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_checkCompatibility_themeOverride.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the (temporary)
+// extensions.checkCompatibility.temporaryThemeOverride_minAppVersion
+// preference works.
+
+var ADDONS = [{
+ id: "addon1@tests.mozilla.org",
+ type: 4,
+ internalName: "theme1/1.0",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1.0",
+ maxVersion: "1.0"
+ }]
+}, {
+ id: "addon2@tests.mozilla.org",
+ type: 4,
+ internalName: "theme2/1.0",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2.0",
+ maxVersion: "2.0"
+ }]
+}];
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3.0", "1");
+
+ for (let a of ADDONS) {
+ writeInstallRDFForExtension(a, profileDir);
+ }
+
+ startupManager();
+
+ run_test_1();
+}
+
+function run_test_1() {
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"],
+ function([a1, a2]) {
+
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_false(a1.isCompatible);
+ do_check_true(a1.appDisabled);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_false(a2.isCompatible);
+ do_check_true(a1.appDisabled);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+function run_test_2() {
+ Services.prefs.setCharPref("extensions.checkCompatibility.temporaryThemeOverride_minAppVersion", "2.0");
+ if (isNightlyChannel())
+ Services.prefs.setBoolPref("extensions.checkCompatibility.nightly", false);
+ else
+ Services.prefs.setBoolPref("extensions.checkCompatibility.3.0", false);
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"],
+ function([a1, a2]) {
+
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_false(a1.isCompatible);
+ do_check_true(a1.appDisabled);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_false(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_checkcompatibility.js b/toolkit/mozapps/extensions/test/xpcshell/test_checkcompatibility.js
new file mode 100644
index 000000000..b9fc0b3ab
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_checkcompatibility.js
@@ -0,0 +1,196 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the extensions.checkCompatibility.* preferences work.
+
+var ADDONS = [{
+ // Cannot be enabled as it has no target app info for the applciation
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "unknown@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+}, {
+ // Always appears incompatible but can be enabled if compatibility checking is
+ // disabled
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+}, {
+ // Always appears incompatible but can be enabled if compatibility checking is
+ // disabled
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+}, { // Always compatible and enabled
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+}, { // Always compatible and enabled
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+}];
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gIsNightly = false;
+
+function run_test() {
+ do_test_pending("checkcompatibility.js");
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2.2.3", "2");
+
+ ADDONS.forEach(function(a) {
+ writeInstallRDFForExtension(a, profileDir);
+ });
+
+ gIsNightly = isNightlyChannel();
+
+ startupManager();
+
+ run_test_1();
+}
+
+/**
+ * Checks that the add-ons are enabled as expected.
+ * @param overridden
+ * A boolean indicating that compatibility checking is overridden
+ * @param a1
+ * The Addon for addon1@tests.mozilla.org
+ * @param a2
+ * The Addon for addon2@tests.mozilla.org
+ * @param a3
+ * The Addon for addon3@tests.mozilla.org
+ * @param a4
+ * The Addon for addon4@tests.mozilla.org
+ * @param a5
+ * The Addon for addon5@tests.mozilla.org
+ */
+function check_state(overridden, a1, a2, a3, a4, a5) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_false(a1.isCompatible);
+
+ do_check_neq(a2, null);
+ if (overridden)
+ do_check_true(a2.isActive);
+ else
+ do_check_false(a2.isActive);
+ do_check_false(a2.isCompatible);
+
+ do_check_neq(a3, null);
+ if (overridden)
+ do_check_true(a3.isActive);
+ else
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_true(a4.isCompatible);
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_true(a5.isCompatible);
+}
+
+// Tests that with compatibility checking enabled we see the incompatible
+// add-ons disabled
+function run_test_1() {
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ check_state(false, a1, a2, a3, a4, a5);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Tests that with compatibility checking disabled we see the incompatible
+// add-ons enabled
+function run_test_2() {
+ if (gIsNightly)
+ Services.prefs.setBoolPref("extensions.checkCompatibility.nightly", false);
+ else
+ Services.prefs.setBoolPref("extensions.checkCompatibility.2.2", false);
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ check_state(true, a1, a2, a3, a4, a5);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Tests that with compatibility checking disabled we see the incompatible
+// add-ons enabled.
+function run_test_3() {
+ if (!gIsNightly)
+ Services.prefs.setBoolPref("extensions.checkCompatibility.2.1a", false);
+ restartManager("2.1a4");
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ check_state(true, a1, a2, a3, a4, a5);
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Tests that with compatibility checking enabled we see the incompatible
+// add-ons disabled.
+function run_test_4() {
+ if (gIsNightly)
+ Services.prefs.setBoolPref("extensions.checkCompatibility.nightly", true);
+ else
+ Services.prefs.setBoolPref("extensions.checkCompatibility.2.1a", true);
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ check_state(false, a1, a2, a3, a4, a5);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js b/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js
new file mode 100644
index 000000000..a6c635eac
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_childprocess.js
@@ -0,0 +1,21 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the AddonManager refuses to load in child processes.
+
+function run_test() {
+ // Already loaded the module by head_addons.js. Need to unload this again, so
+ // that overriding the app-info and re-importing the module works.
+ Components.utils.unload("resource://gre/modules/AddonManager.jsm");
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ gAppInfo.processType = AM_Ci.nsIXULRuntime.PROCESS_TYPE_CONTENT;
+ try {
+ Components.utils.import("resource://gre/modules/AddonManager.jsm");
+ do_throw("AddonManager should have refused to load");
+ }
+ catch (ex) {
+ do_print(ex.message);
+ do_check_true(!!ex.message);
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js b/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js
new file mode 100644
index 000000000..c079534c3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_compatoverrides.js
@@ -0,0 +1,259 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests compatibility overrides, for when strict compatibility checking is
+// disabled. See bug 693906.
+
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+
+const PORT = gPort;
+const BASE_URL = "http://localhost:" + PORT;
+const DEFAULT_URL = "about:blank";
+const REQ_URL = "/data.xml";
+
+// register static file and mark it for interpolation
+mapUrlToFile(REQ_URL, do_get_file("data/test_compatoverrides.xml"), gServer);
+
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE,
+ BASE_URL + REQ_URL);
+
+
+// Not hosted, no overrides
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, no overrides
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, matching override
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, matching override, wouldn't be compatible if strict checking is enabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }]
+};
+
+// Hosted, app ID doesn't match in override
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, addon version range doesn't match in override
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 6",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, app version range doesn't match in override
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 7",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Hosted, multiple overrides
+var addon8 = {
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 8",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Not hosted, matching override
+var addon9 = {
+ id: "addon9@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 9",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Not hosted, override is of unsupported type (compatible)
+var addon10 = {
+ id: "addon10@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon 10",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(addon8, profileDir);
+ writeInstallRDFForExtension(addon9, profileDir);
+ writeInstallRDFForExtension(addon10, profileDir);
+
+ startupManager();
+
+ AddonManagerInternal.backgroundUpdateCheck().then(run_test_1);
+}
+
+function end_test() {
+ gServer.stop(do_test_finished);
+}
+
+function check_compat_status(aCallback) {
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "addon8@tests.mozilla.org",
+ "addon9@tests.mozilla.org",
+ "addon10@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.compatibilityOverrides, null);
+ do_check_true(a1.isCompatible);
+ do_check_false(a1.appDisabled);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.compatibilityOverrides, null);
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+
+ do_check_neq(a3, null);
+ do_check_neq(a3.compatibilityOverrides, null);
+ do_check_eq(a3.compatibilityOverrides.length, 1);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+
+ do_check_neq(a4, null);
+ do_check_neq(a4.compatibilityOverrides, null);
+ do_check_eq(a4.compatibilityOverrides.length, 1);
+ do_check_false(a4.isCompatible);
+ do_check_true(a4.appDisabled);
+
+ do_check_neq(a5, null);
+ do_check_eq(a5.compatibilityOverrides, null);
+ do_check_true(a5.isCompatible);
+ do_check_false(a5.appDisabled);
+
+ do_check_neq(a6, null);
+ do_check_neq(a6.compatibilityOverrides, null);
+ do_check_eq(a6.compatibilityOverrides.length, 1);
+ do_check_true(a6.isCompatible);
+ do_check_false(a6.appDisabled);
+
+ do_check_neq(a7, null);
+ do_check_neq(a7.compatibilityOverrides, null);
+ do_check_eq(a7.compatibilityOverrides.length, 1);
+ do_check_true(a7.isCompatible);
+ do_check_false(a7.appDisabled);
+
+ do_check_neq(a8, null);
+ do_check_neq(a8.compatibilityOverrides, null);
+ do_check_eq(a8.compatibilityOverrides.length, 3);
+ do_check_false(a8.isCompatible);
+ do_check_true(a8.appDisabled);
+
+ do_check_neq(a9, null);
+ do_check_neq(a9.compatibilityOverrides, null);
+ do_check_eq(a9.compatibilityOverrides.length, 1);
+ do_check_false(a9.isCompatible);
+ do_check_true(a9.appDisabled);
+
+ do_check_neq(a10, null);
+ do_check_eq(a10.compatibilityOverrides, null);
+ do_check_true(a10.isCompatible);
+ do_check_false(a10.appDisabled);
+
+ do_execute_soon(aCallback);
+ });
+}
+
+function run_test_1() {
+ do_print("Run test 1");
+ check_compat_status(run_test_2);
+}
+
+function run_test_2() {
+ do_print("Run test 2");
+ restartManager();
+ check_compat_status(end_test);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
new file mode 100644
index 000000000..210c6a936
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt.js
@@ -0,0 +1,406 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we rebuild something sensible from a corrupt database
+
+
+Components.utils.import("resource://testing-common/httpd.js");
+// Create and configure the HTTP server.
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register files with server
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+mapFile("/data/test_corrupt.rdf", testserver);
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+// Will be enabled
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will get a compatibility update and stay enabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Will get a compatibility update and be enabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Would stay incompatible with strict compat
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Enabled bootstrapped
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Disabled bootstrapped
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The default theme
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The selected theme
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a2, a3, a4,
+ a7, t2]) {
+ // Set up the initial state
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ a7.userDisabled = true;
+ t2.userDisabled = false;
+ a3.findUpdates({
+ onUpdateFinished: function() {
+ a4.findUpdates({
+ onUpdateFinished: function() {
+ do_execute_soon(run_test_1);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ });
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test_1() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Shutdown and replace the database with a corrupt file (a directory
+ // serves this purpose). On startup the add-ons manager won't rebuild
+ // because there is a file there still.
+ shutdownManager();
+ gExtensionsJSON.remove(true);
+ gExtensionsJSON.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ startupManager(false);
+
+ // Accessing the add-ons should open and recover the database
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1_2, a2_2, a3_2, a4_2, a5_2, a6_2, a7_2, t1_2, t2_2]) {
+ // Should be correctly recovered
+ do_check_neq(a1_2, null);
+ do_check_true(a1_2.isActive);
+ do_check_false(a1_2.userDisabled);
+ do_check_false(a1_2.appDisabled);
+ do_check_eq(a1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(a2_2, null);
+ do_check_false(a2_2.isActive);
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.appDisabled);
+ do_check_eq(a2_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // The compatibility update won't be recovered but it should still be
+ // active for this session
+ do_check_neq(a3_2, null);
+ do_check_true(a3_2.isActive);
+ do_check_false(a3_2.userDisabled);
+ do_check_false(a3_2.appDisabled);
+ do_check_eq(a3_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // The compatibility update won't be recovered and with strict
+ // compatibility it would not have been able to tell that it was
+ // previously userDisabled. However, without strict compat, it wasn't
+ // appDisabled, so it knows it must have been userDisabled.
+ do_check_neq(a4_2, null);
+ do_check_false(a4_2.isActive);
+ do_check_true(a4_2.userDisabled);
+ do_check_false(a4_2.appDisabled);
+ do_check_eq(a4_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5_2, null);
+ do_check_true(a5_2.isActive);
+ do_check_false(a5_2.userDisabled);
+ do_check_false(a5_2.appDisabled);
+ do_check_eq(a5_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6_2, null);
+ do_check_true(a6_2.isActive);
+ do_check_false(a6_2.userDisabled);
+ do_check_false(a6_2.appDisabled);
+ do_check_eq(a6_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7_2, null);
+ do_check_false(a7_2.isActive);
+ do_check_true(a7_2.userDisabled);
+ do_check_false(a7_2.appDisabled);
+ do_check_eq(a7_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t1_2, null);
+ do_check_false(t1_2.isActive);
+ do_check_true(t1_2.userDisabled);
+ do_check_false(t1_2.appDisabled);
+ do_check_eq(t1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t2_2, null);
+ do_check_true(t2_2.isActive);
+ do_check_false(t2_2.userDisabled);
+ do_check_false(t2_2.appDisabled);
+ do_check_eq(t2_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ Assert.throws(shutdownManager);
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1_3, a2_3, a3_3, a4_3, a5_3, a6_3, a7_3, t1_3, t2_3]) {
+ do_check_neq(a1_3, null);
+ do_check_true(a1_3.isActive);
+ do_check_false(a1_3.userDisabled);
+ do_check_false(a1_3.appDisabled);
+ do_check_eq(a1_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2_3, null);
+ do_check_false(a2_3.isActive);
+ do_check_true(a2_3.userDisabled);
+ do_check_false(a2_3.appDisabled);
+ do_check_eq(a2_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3_3, null);
+ do_check_true(a3_3.isActive);
+ do_check_false(a3_3.userDisabled);
+ do_check_false(a3_3.appDisabled);
+ do_check_eq(a3_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4_3, null);
+ do_check_false(a4_3.isActive);
+ do_check_true(a4_3.userDisabled);
+ do_check_false(a4_3.appDisabled);
+ do_check_eq(a4_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5_3, null);
+ do_check_true(a5_3.isActive);
+ do_check_false(a5_3.userDisabled);
+ do_check_false(a5_3.appDisabled);
+ do_check_eq(a5_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6_3, null);
+ do_check_true(a6_3.isActive);
+ do_check_false(a6_3.userDisabled);
+ do_check_false(a6_3.appDisabled);
+ do_check_eq(a6_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7_3, null);
+ do_check_false(a7_3.isActive);
+ do_check_true(a7_3.userDisabled);
+ do_check_false(a7_3.appDisabled);
+ do_check_eq(a7_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1_3, null);
+ do_check_false(t1_3.isActive);
+ do_check_true(t1_3.userDisabled);
+ do_check_false(t1_3.appDisabled);
+ do_check_eq(t1_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2_3, null);
+ do_check_true(t2_3.isActive);
+ do_check_false(t2_3.userDisabled);
+ do_check_false(t2_3.appDisabled);
+ do_check_eq(t2_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ Assert.throws(shutdownManager);
+
+ end_test();
+ }));
+ }));
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js
new file mode 100644
index 000000000..622973472
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_corrupt_strictcompat.js
@@ -0,0 +1,405 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we rebuild something sensible from a corrupt database
+
+
+Components.utils.import("resource://testing-common/httpd.js");
+// Create and configure the HTTP server.
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register files with server
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+mapFile("/data/test_corrupt.rdf", testserver);
+
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+// Will be enabled
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will get a compatibility update and be enabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Will get a compatibility update and be disabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Stays incompatible
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Enabled bootstrapped
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Disabled bootstrapped
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The default theme
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The selected theme
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a2, a3, a4,
+ a7, t2]) {
+ // Set up the initial state
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ a7.userDisabled = true;
+ t2.userDisabled = false;
+ a3.findUpdates({
+ onUpdateFinished: function() {
+ a4.findUpdates({
+ onUpdateFinished: function() {
+ do_execute_soon(run_test_1);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ });
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+function run_test_1() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1, a2, a3, a4, a5, a6, a7, t1, t2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Shutdown and replace the database with a corrupt file (a directory
+ // serves this purpose). On startup the add-ons manager won't rebuild
+ // because there is a file there still.
+ shutdownManager();
+ gExtensionsJSON.remove(true);
+ gExtensionsJSON.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ startupManager(false);
+
+ // Accessing the add-ons should open and recover the database
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1_2, a2_2, a3_2, a4_2, a5_2, a6_2, a7_2, t1_2, t2_2]) {
+ // Should be correctly recovered
+ do_check_neq(a1_2, null);
+ do_check_true(a1_2.isActive);
+ do_check_false(a1_2.userDisabled);
+ do_check_false(a1_2.appDisabled);
+ do_check_eq(a1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(a2_2, null);
+ do_check_false(a2_2.isActive);
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.appDisabled);
+ do_check_eq(a2_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // The compatibility update won't be recovered but it should still be
+ // active for this session
+ do_check_neq(a3_2, null);
+ do_check_true(a3_2.isActive);
+ do_check_false(a3_2.userDisabled);
+ do_check_true(a3_2.appDisabled);
+ do_check_eq(a3_2.pendingOperations, AddonManager.PENDING_DISABLE);
+
+ // The compatibility update won't be recovered and it will not have been
+ // able to tell that it was previously userDisabled
+ do_check_neq(a4_2, null);
+ do_check_false(a4_2.isActive);
+ do_check_false(a4_2.userDisabled);
+ do_check_true(a4_2.appDisabled);
+ do_check_eq(a4_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5_2, null);
+ do_check_false(a5_2.isActive);
+ do_check_false(a5_2.userDisabled);
+ do_check_true(a5_2.appDisabled);
+ do_check_eq(a5_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6_2, null);
+ do_check_true(a6_2.isActive);
+ do_check_false(a6_2.userDisabled);
+ do_check_false(a6_2.appDisabled);
+ do_check_eq(a6_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7_2, null);
+ do_check_false(a7_2.isActive);
+ do_check_true(a7_2.userDisabled);
+ do_check_false(a7_2.appDisabled);
+ do_check_eq(a7_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t1_2, null);
+ do_check_false(t1_2.isActive);
+ do_check_true(t1_2.userDisabled);
+ do_check_false(t1_2.appDisabled);
+ do_check_eq(t1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t2_2, null);
+ do_check_true(t2_2.isActive);
+ do_check_false(t2_2.userDisabled);
+ do_check_false(t2_2.appDisabled);
+ do_check_eq(t2_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ Assert.throws(shutdownManager);
+ startupManager(false);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([a1_3, a2_3, a3_3, a4_3, a5_3, a6_3, a7_3, t1_3, t2_3]) {
+ do_check_neq(a1_3, null);
+ do_check_true(a1_3.isActive);
+ do_check_false(a1_3.userDisabled);
+ do_check_false(a1_3.appDisabled);
+ do_check_eq(a1_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a2_3, null);
+ do_check_false(a2_3.isActive);
+ do_check_true(a2_3.userDisabled);
+ do_check_false(a2_3.appDisabled);
+ do_check_eq(a2_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a3_3, null);
+ do_check_false(a3_3.isActive);
+ do_check_false(a3_3.userDisabled);
+ do_check_true(a3_3.appDisabled);
+ do_check_eq(a3_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4_3, null);
+ do_check_false(a4_3.isActive);
+ do_check_false(a4_3.userDisabled);
+ do_check_true(a4_3.appDisabled);
+ do_check_eq(a4_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5_3, null);
+ do_check_false(a5_3.isActive);
+ do_check_false(a5_3.userDisabled);
+ do_check_true(a5_3.appDisabled);
+ do_check_eq(a5_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a6_3, null);
+ do_check_true(a6_3.isActive);
+ do_check_false(a6_3.userDisabled);
+ do_check_false(a6_3.appDisabled);
+ do_check_eq(a6_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7_3, null);
+ do_check_false(a7_3.isActive);
+ do_check_true(a7_3.userDisabled);
+ do_check_false(a7_3.appDisabled);
+ do_check_eq(a7_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1_3, null);
+ do_check_false(t1_3.isActive);
+ do_check_true(t1_3.userDisabled);
+ do_check_false(t1_3.appDisabled);
+ do_check_eq(t1_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t2_3, null);
+ do_check_true(t2_3.isActive);
+ do_check_false(t2_3.userDisabled);
+ do_check_false(t2_3.appDisabled);
+ do_check_eq(t2_3.pendingOperations, AddonManager.PENDING_NONE);
+
+ Assert.throws(shutdownManager);
+
+ end_test();
+ }));
+ }));
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_corruptfile.js b/toolkit/mozapps/extensions/test/xpcshell/test_corruptfile.js
new file mode 100644
index 000000000..92b375850
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_corruptfile.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that attempting to install a corrupt XPI file doesn't break the universe
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ startupManager();
+
+ if (TEST_UNPACKED)
+ run_test_unpacked();
+ else
+ run_test_packed();
+}
+
+// When installing packed we won't detect corruption in the XPI until we attempt
+// to load bootstrap.js so everything will look normal from the outside.
+function run_test_packed() {
+ do_test_pending();
+
+ prepare_test({
+ "corrupt@tests.mozilla.org": [
+ ["onInstalling", false],
+ ["onInstalled", false]
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+
+ installAllFiles([do_get_file("data/corruptfile.xpi")], function() {
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("corrupt@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+
+ do_test_finished();
+ });
+ });
+}
+
+// When extracting the corruption will be detected and the add-on fails to
+// install
+function run_test_unpacked() {
+ do_test_pending();
+
+ prepare_test({
+ "corrupt@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onOperationCancelled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallFailed"
+ ]);
+
+ installAllFiles([do_get_file("data/corruptfile.xpi")], function() {
+ ensure_test_completed();
+
+ // Check the add-on directory isn't left over
+ var addonDir = profileDir.clone();
+ addonDir.append("corrupt@tests.mozilla.org");
+ pathShouldntExist(addonDir);
+
+ // Check the staging directory isn't left over
+ var stageDir = profileDir.clone();
+ stageDir.append("staged");
+ pathShouldntExist(stageDir);
+
+ AddonManager.getAddonByID("corrupt@tests.mozilla.org", function(addon) {
+ do_check_eq(addon, null);
+
+ do_test_finished();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dataDirectory.js b/toolkit/mozapps/extensions/test/xpcshell/test_dataDirectory.js
new file mode 100644
index 000000000..bf75818e9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_dataDirectory.js
@@ -0,0 +1,50 @@
+/* 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/.
+ */
+
+// Disables security checking our updates which haven't been signed
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+var ADDON = {
+ id: "datadirectory1@tests.mozilla.org",
+ addon: "test_data_directory"
+};
+
+function run_test() {
+ var expectedDir = gProfD.clone();
+ expectedDir.append("extension-data");
+ expectedDir.append(ADDON.id);
+
+ do_test_pending();
+ do_check_false(expectedDir.exists());
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "1.9");
+ startupManager();
+
+ installAllFiles([do_get_addon(ADDON.addon)], function() {
+ restartManager();
+
+ AddonManager.getAddonByID(ADDON.id, function(item) {
+ item.getDataDirectory(promise_callback);
+ });
+ });
+}
+
+function promise_callback() {
+ do_check_eq(arguments.length, 2);
+ var expectedDir = gProfD.clone();
+ expectedDir.append("extension-data");
+ expectedDir.append(ADDON.id);
+
+ do_check_eq(arguments[0], expectedDir.path);
+ do_check_true(expectedDir.exists());
+ do_check_true(expectedDir.isDirectory());
+
+ do_check_eq(arguments[1], null);
+
+ // Cleanup.
+ expectedDir.parent.remove(true);
+
+ do_test_finished();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js b/toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
new file mode 100644
index 000000000..1b61e033a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_default_providers_pref.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the extensions.defaultProviders.enabled pref which turns
+// off the default XPIProvider and LightweightThemeManager.
+
+function run_test() {
+ Services.prefs.setBoolPref("extensions.defaultProviders.enabled", false);
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+ do_check_false(AddonManager.isInstallEnabled("application/x-xpinstall"));
+ Services.prefs.clearUserPref("extensions.defaultProviders.enabled");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update.js
new file mode 100644
index 000000000..3d7eef051
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update.js
@@ -0,0 +1,260 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that delaying an update works
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const tempdir = gTmpD.clone();
+
+const IGNORE_ID = "test_delay_update_ignore@tests.mozilla.org";
+const COMPLETE_ID = "test_delay_update_complete@tests.mozilla.org";
+const DEFER_ID = "test_delay_update_defer@tests.mozilla.org";
+
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+// Note that we would normally use BootstrapMonitor but it currently requires
+// the objects in `data` to be serializable, and we need a real reference to the
+// `instanceID` symbol to test.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+// Create and configure the HTTP server.
+let testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_delay_updates_complete.rdf", testserver);
+mapFile("/data/test_delay_updates_ignore.rdf", testserver);
+mapFile("/data/test_delay_updates_defer.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+function* createIgnoreAddon() {
+ writeInstallRDFToDir({
+ id: IGNORE_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_ignore.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Ignore",
+ }, profileDir, IGNORE_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(IGNORE_ID);
+ do_get_file("data/test_delay_update_ignore/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+function* createCompleteAddon() {
+ writeInstallRDFToDir({
+ id: COMPLETE_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_complete.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Complete",
+ }, profileDir, COMPLETE_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(COMPLETE_ID);
+ do_get_file("data/test_delay_update_complete/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+function* createDeferAddon() {
+ writeInstallRDFToDir({
+ id: DEFER_ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ updateURL: `http://localhost:${gPort}/data/test_delay_updates_defer.rdf`,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Delay Update Defer",
+ }, profileDir, DEFER_ID, "bootstrap.js");
+
+ let unpacked_addon = profileDir.clone();
+ unpacked_addon.append(DEFER_ID);
+ do_get_file("data/test_delay_update_defer/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+}
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function*() {
+
+ yield createIgnoreAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Ignore");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_POSTPONED);
+
+ // addon upgrade has been delayed
+ let addon_postponed = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Ignore");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+ do_check_true(Services.prefs.getBoolPref(TEST_IGNORE_PREF));
+
+ // restarting allows upgrade to proceed
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Ignore");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, and allows update.
+add_task(function*() {
+
+ yield createCompleteAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Complete");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Complete");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseAddonEvent("onInstalled");
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Test Delay Update Complete");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Complete");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, initially defers update then allows upgrade
+add_task(function*() {
+
+ yield createDeferAddon();
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Delay Update Defer");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Test Delay Update Defer");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // add-on will not allow upgrade until fake event fires
+ AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseAddonEvent("onInstalled");
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Test Delay Update Defer");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Test Delay Update Defer");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
new file mode 100644
index 000000000..cdfac8f8c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_delay_update_webextension.js
@@ -0,0 +1,344 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that delaying an update works for WebExtensions.
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+/* globals browser*/
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const tempdir = gTmpD.clone();
+const stageDir = profileDir.clone();
+stageDir.append("staged");
+
+const IGNORE_ID = "test_delay_update_ignore_webext@tests.mozilla.org";
+const COMPLETE_ID = "test_delay_update_complete_webext@tests.mozilla.org";
+const DEFER_ID = "test_delay_update_defer_webext@tests.mozilla.org";
+const NOUPDATE_ID = "test_no_update_webext@tests.mozilla.org";
+
+// Create and configure the HTTP server.
+let testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_delay_updates_complete.json", testserver);
+mapFile("/data/test_delay_updates_ignore.json", testserver);
+mapFile("/data/test_delay_updates_defer.json", testserver);
+mapFile("/data/test_no_update.json", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseWebExtensionStartup() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function* delay_updates_ignore() {
+ startupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": IGNORE_ID,
+ "update_url": `http://localhost:${gPort}/data/test_delay_updates_ignore.json`,
+ },
+ },
+ },
+ background() {
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ if (details) {
+ if (details.version) {
+ // This should be the version of the pending update.
+ browser.test.assertEq("2.0", details.version, "correct version");
+ browser.test.notifyPass("delay");
+ }
+ } else {
+ browser.test.fail("no details object passed");
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ }, IGNORE_ID);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ let addon = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Generated extension");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_POSTPONED);
+
+ // addon upgrade has been delayed
+ let addon_postponed = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Generated extension");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ yield extension.awaitFinish("delay");
+
+ // restarting allows upgrade to proceed
+ yield extension.markUnloaded();
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
+ yield promiseWebExtensionStartup();
+
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "Delay Upgrade");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield addon_upgraded.uninstall();
+ yield promiseShutdownManager();
+});
+
+// add-on registers upgrade listener, and allows update.
+add_task(function* delay_updates_complete() {
+ startupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": COMPLETE_ID,
+ "update_url": `http://localhost:${gPort}/data/test_delay_updates_complete.json`,
+ },
+ },
+ },
+ background() {
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ browser.test.notifyPass("reload");
+ browser.runtime.reload();
+ });
+ browser.test.sendMessage("ready");
+ },
+ }, COMPLETE_ID);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ let addon = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Generated extension");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ let promiseInstalled = promiseAddonEvent("onInstalled");
+ yield promiseCompleteAllInstalls([install]);
+
+ yield extension.awaitFinish("reload");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseInstalled;
+ yield promiseWebExtensionStartup();
+
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Delay Upgrade");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ if (stageDir.exists()) {
+ do_throw("Staging directory should not exist for formerly-postponed extension");
+ }
+
+ yield extension.markUnloaded();
+ yield addon_allowed.uninstall();
+ yield promiseShutdownManager();
+});
+
+// add-on registers upgrade listener, initially defers update then allows upgrade
+add_task(function* delay_updates_defer() {
+ startupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": DEFER_ID,
+ "update_url": `http://localhost:${gPort}/data/test_delay_updates_defer.json`,
+ },
+ },
+ },
+ background() {
+ browser.runtime.onUpdateAvailable.addListener(details => {
+ // Upgrade will only proceed when "allow" message received.
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "allow") {
+ browser.test.notifyPass("allowed");
+ browser.runtime.reload();
+ } else {
+ browser.test.fail(`wrong message: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("truly ready");
+ });
+ browser.test.sendMessage("ready");
+ },
+ }, DEFER_ID);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ let addon = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Generated extension");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+
+ let promiseInstalled = promiseAddonEvent("onInstalled");
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_POSTPONED);
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "Generated extension");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // add-on will not allow upgrade until message is received
+ yield extension.awaitMessage("truly ready");
+ extension.sendMessage("allow");
+ yield extension.awaitFinish("allowed");
+
+ // addon upgrade has been allowed
+ let [addon_allowed] = yield promiseInstalled;
+ yield promiseWebExtensionStartup();
+
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Delay Upgrade");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ yield extension.markUnloaded();
+ yield promiseRestartManager();
+
+ // restart changes nothing
+ addon_allowed = yield promiseAddonByID(DEFER_ID);
+ yield promiseWebExtensionStartup();
+
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "Delay Upgrade");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ yield addon_allowed.uninstall();
+ yield promiseShutdownManager();
+});
+
+// browser.runtime.reload() without a pending upgrade should just reload.
+add_task(function* runtime_reload() {
+ startupManager();
+
+ let extension = ExtensionTestUtils.loadExtension({
+ useAddonManager: "permanent",
+ manifest: {
+ "version": "1.0",
+ "applications": {
+ "gecko": {
+ "id": NOUPDATE_ID,
+ "update_url": `http://localhost:${gPort}/data/test_no_update.json`,
+ },
+ },
+ },
+ background() {
+ browser.test.onMessage.addListener(msg => {
+ if (msg == "reload") {
+ browser.runtime.reload();
+ } else {
+ browser.test.fail(`wrong message: ${msg}`);
+ }
+ });
+ browser.test.sendMessage("ready");
+ },
+ }, NOUPDATE_ID);
+
+ yield Promise.all([extension.startup(), extension.awaitMessage("ready")]);
+
+ let addon = yield promiseAddonByID(NOUPDATE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Generated extension");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ yield promiseFindAddonUpdates(addon);
+
+ extension.sendMessage("reload");
+ // Wait for extension to restart, to make sure reload works.
+ yield promiseWebExtensionStartup();
+
+ addon = yield promiseAddonByID(NOUPDATE_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Generated extension");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ yield extension.markUnloaded();
+ yield addon.uninstall();
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js
new file mode 100644
index 000000000..3afc03f84
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_dependencies.js
@@ -0,0 +1,144 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+startupManager();
+
+const BOOTSTRAP = String.raw`
+ Components.utils.import("resource://gre/modules/Services.jsm");
+
+ function startup(data) {
+ Services.obs.notifyObservers(null, "test-addon-bootstrap-startup", data.id);
+ }
+ function shutdown(data) {
+ Services.obs.notifyObservers(null, "test-addon-bootstrap-shutdown", data.id);
+ }
+ function install() {}
+ function uninstall() {}
+`;
+
+const ADDONS = [
+ {
+ id: "addon1@dependency-test.mozilla.org",
+ dependencies: ["addon2@dependency-test.mozilla.org"],
+ },
+ {
+ id: "addon2@dependency-test.mozilla.org",
+ dependencies: ["addon3@dependency-test.mozilla.org"],
+ },
+ {
+ id: "addon3@dependency-test.mozilla.org",
+ },
+ {
+ id: "addon4@dependency-test.mozilla.org",
+ },
+ {
+ id: "addon5@dependency-test.mozilla.org",
+ dependencies: ["addon2@dependency-test.mozilla.org"],
+ },
+];
+
+let addonFiles = [];
+
+let events = [];
+add_task(function* setup() {
+ let startupObserver = (subject, topic, data) => {
+ events.push(["startup", data]);
+ };
+ let shutdownObserver = (subject, topic, data) => {
+ events.push(["shutdown", data]);
+ };
+
+ Services.obs.addObserver(startupObserver, "test-addon-bootstrap-startup", false);
+ Services.obs.addObserver(shutdownObserver, "test-addon-bootstrap-shutdown", false);
+ do_register_cleanup(() => {
+ Services.obs.removeObserver(startupObserver, "test-addon-bootstrap-startup");
+ Services.obs.removeObserver(shutdownObserver, "test-addon-bootstrap-shutdown");
+ });
+
+ for (let addon of ADDONS) {
+ Object.assign(addon, {
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1",
+ }],
+ version: "1.0",
+ name: addon.id,
+ bootstrap: true,
+ });
+
+ addonFiles.push(createTempXPIFile(addon, {"bootstrap.js": BOOTSTRAP}));
+ }
+});
+
+add_task(function*() {
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[3]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[3].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[0]]);
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[1]]);
+ deepEqual(events, [], "Should have no events");
+
+ yield promiseInstallAllFiles([addonFiles[2]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[2]]);
+
+ deepEqual(events, [
+ ["shutdown", ADDONS[0].id],
+ ["shutdown", ADDONS[1].id],
+ ["shutdown", ADDONS[2].id],
+
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseInstallAllFiles([addonFiles[4]]);
+
+ deepEqual(events, [
+ ["startup", ADDONS[4].id],
+ ]);
+
+ events.length = 0;
+
+ yield promiseRestartManager();
+
+ deepEqual(events, [
+ ["shutdown", ADDONS[4].id],
+ ["shutdown", ADDONS[3].id],
+ ["shutdown", ADDONS[0].id],
+ ["shutdown", ADDONS[1].id],
+ ["shutdown", ADDONS[2].id],
+
+ ["startup", ADDONS[2].id],
+ ["startup", ADDONS[1].id],
+ ["startup", ADDONS[0].id],
+ ["startup", ADDONS[3].id],
+ ["startup", ADDONS[4].id],
+ ]);
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dictionary.js b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary.js
new file mode 100644
index 000000000..f4b6a0535
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_dictionary.js
@@ -0,0 +1,811 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that bootstrappable add-ons can be used without restarts.
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// Enable loading extensions from the user scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const userExtDir = gProfD.clone();
+userExtDir.append("extensions2");
+userExtDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userExtDir.parent);
+
+Components.utils.import("resource://testing-common/httpd.js");
+// Create and configure the HTTP server.
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// register files with server
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+mapFile("/data/test_dictionary.rdf", testserver);
+
+/**
+ * This object is both a factory and an mozISpellCheckingEngine implementation (so, it
+ * is de-facto a service). It's also an interface requestor that gives out
+ * itself when asked for mozISpellCheckingEngine.
+ */
+var HunspellEngine = {
+ dictionaryDirs: [],
+ listener: null,
+
+ QueryInterface: function hunspell_qi(iid) {
+ if (iid.equals(Components.interfaces.nsISupports) ||
+ iid.equals(Components.interfaces.nsIFactory) ||
+ iid.equals(Components.interfaces.mozISpellCheckingEngine))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+ createInstance: function hunspell_ci(outer, iid) {
+ if (outer)
+ throw Components.results.NS_ERROR_NO_AGGREGATION;
+ return this.QueryInterface(iid);
+ },
+ lockFactory: function hunspell_lockf(lock) {
+ throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ addDirectory: function hunspell_addDirectory(dir) {
+ this.dictionaryDirs.push(dir);
+ if (this.listener)
+ this.listener("addDirectory");
+ },
+
+ removeDirectory: function hunspell_addDirectory(dir) {
+ this.dictionaryDirs.splice(this.dictionaryDirs.indexOf(dir), 1);
+ if (this.listener)
+ this.listener("removeDirectory");
+ },
+
+ getInterface: function hunspell_gi(iid) {
+ if (iid.equals(Components.interfaces.mozISpellCheckingEngine))
+ return this;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ },
+
+ contractID: "@mozilla.org/spellchecker/engine;1",
+ classID: Components.ID("{6f3c63bc-a4fd-449b-9a58-a2d9bd972cce}"),
+
+ activate: function hunspell_activate() {
+ this.origClassID = Components.manager.nsIComponentRegistrar
+ .contractIDToCID(this.contractID);
+ this.origFactory = Components.manager
+ .getClassObject(Components.classes[this.contractID],
+ Components.interfaces.nsIFactory);
+
+ Components.manager.nsIComponentRegistrar
+ .unregisterFactory(this.origClassID, this.origFactory);
+ Components.manager.nsIComponentRegistrar.registerFactory(this.classID,
+ "Test hunspell", this.contractID, this);
+ },
+
+ deactivate: function hunspell_deactivate() {
+ Components.manager.nsIComponentRegistrar.unregisterFactory(this.classID, this);
+ Components.manager.nsIComponentRegistrar.registerFactory(this.origClassID,
+ "Hunspell", this.contractID, this.origFactory);
+ },
+
+ isDictionaryEnabled: function hunspell_isDictionaryEnabled(name) {
+ return this.dictionaryDirs.some(function(dir) {
+ var dic = dir.clone();
+ dic.append(name);
+ return dic.exists();
+ });
+ }
+};
+
+function run_test() {
+ do_test_pending();
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Tests that installing doesn't require a restart
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ HunspellEngine.activate();
+
+ AddonManager.getInstallForFile(do_get_addon("test_dictionary"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "dictionary");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Dictionary");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_false(install.addon.hasResource("bootstrap.js"));
+ do_check_eq(install.addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL, 0);
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ let addon = install.addon;
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_check_true(addon.hasResource("install.rdf"));
+ HunspellEngine.listener = function(aEvent) {
+ HunspellEngine.listener = null;
+ do_check_eq(aEvent, "addDirectory");
+ do_execute_soon(check_test_1);
+ };
+ });
+ install.install();
+ });
+}
+
+function check_test_1() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_false(b1.hasResource("bootstrap.js"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ let dir = do_get_addon_root_uri(profileDir, "ab-CD@dictionaries.addons.mozilla.org");
+
+ let chromeReg = AM_Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(AM_Ci.nsIChromeRegistry);
+ try {
+ chromeReg.convertChromeURL(NetUtil.newURI("chrome://dict/content/dict.xul"));
+ do_throw("Chrome manifest should not have been registered");
+ }
+ catch (e) {
+ // Expected the chrome url to not be registered
+ }
+
+ AddonManager.getAddonsWithOperationsByTypes(null, function(list) {
+ do_check_eq(list.length, 0);
+
+ run_test_2();
+ });
+ });
+ });
+}
+
+// Tests that disabling doesn't require a restart
+function run_test_2() {
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_DISABLE, 0);
+ b1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_true(newb1.userDisabled);
+ do_check_false(newb1.isActive);
+
+ do_execute_soon(run_test_3);
+ });
+ });
+}
+
+// Test that restarting doesn't accidentally re-enable
+function run_test_3() {
+ shutdownManager();
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ startupManager(false);
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+
+ run_test_4();
+ });
+}
+
+// Tests that enabling doesn't require a restart
+function run_test_4() {
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_ENABLE, 0);
+ b1.userDisabled = false;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_false(newb1.userDisabled);
+ do_check_true(newb1.isActive);
+
+ do_execute_soon(run_test_5);
+ });
+ });
+}
+
+// Tests that a restart shuts down and restarts the add-on
+function run_test_5() {
+ shutdownManager();
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+ startupManager(false);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, b1.id));
+
+ run_test_7();
+ });
+}
+
+// Tests that uninstalling doesn't require a restart
+function run_test_7() {
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+ b1.uninstall();
+
+ check_test_7();
+ });
+}
+
+function check_test_7() {
+ ensure_test_completed();
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1) {
+ do_check_eq(b1, null);
+
+ restartManager();
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(newb1) {
+ do_check_eq(newb1, null);
+
+ do_execute_soon(run_test_8);
+ });
+ }));
+}
+
+// Test that a bootstrapped extension dropped into the profile loads properly
+// on startup and doesn't cause an EM restart
+function run_test_8() {
+ shutdownManager();
+
+ let dir = profileDir.clone();
+ dir.append("ab-CD@dictionaries.addons.mozilla.org");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(AM_Ci.nsIZipReader);
+ zip.open(do_get_addon("test_dictionary"));
+ dir.append("install.rdf");
+ zip.extract("install.rdf", dir);
+ dir = dir.parent;
+ dir.append("dictionaries");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ dir.append("ab-CD.dic");
+ zip.extract("dictionaries/ab-CD.dic", dir);
+ zip.close();
+
+ startupManager(false);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ do_execute_soon(run_test_9);
+ });
+}
+
+// Test that items detected as removed during startup get removed properly
+function run_test_9() {
+ shutdownManager();
+
+ let dir = profileDir.clone();
+ dir.append("ab-CD@dictionaries.addons.mozilla.org");
+ dir.remove(true);
+ startupManager(false);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_eq(b1, null);
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+
+// Tests that bootstrapped extensions are correctly loaded even if the app is
+// upgraded at the same time
+function run_test_12() {
+ shutdownManager();
+
+ let dir = profileDir.clone();
+ dir.append("ab-CD@dictionaries.addons.mozilla.org");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(AM_Ci.nsIZipReader);
+ zip.open(do_get_addon("test_dictionary"));
+ dir.append("install.rdf");
+ zip.extract("install.rdf", dir);
+ dir = dir.parent;
+ dir.append("dictionaries");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ dir.append("ab-CD.dic");
+ zip.extract("dictionaries/ab-CD.dic", dir);
+ zip.close();
+
+ startupManager(true);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ b1.uninstall();
+ do_execute_soon(run_test_16);
+ });
+}
+
+
+// Tests that bootstrapped extensions don't get loaded when in safe mode
+function run_test_16() {
+ restartManager();
+
+ installAllFiles([do_get_addon("test_dictionary")], function() {
+ // spin the event loop to let the addon finish starting
+ do_execute_soon(function check_installed_dictionary() {
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1) {
+ // Should have installed and started
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ shutdownManager();
+
+ // Should have stopped
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ gAppInfo.inSafeMode = true;
+ startupManager(false);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1_2) {
+ // Should still be stopped
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_false(b1_2.isActive);
+
+ shutdownManager();
+ gAppInfo.inSafeMode = false;
+ startupManager(false);
+
+ // Should have started
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1_3) {
+ b1_3.uninstall();
+
+ do_execute_soon(run_test_17);
+ });
+ }));
+ }));
+ });
+ });
+}
+
+// Check that a bootstrapped extension in a non-profile location is loaded
+function run_test_17() {
+ shutdownManager();
+
+ let dir = userExtDir.clone();
+ dir.append("ab-CD@dictionaries.addons.mozilla.org");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ let zip = AM_Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(AM_Ci.nsIZipReader);
+ zip.open(do_get_addon("test_dictionary"));
+ dir.append("install.rdf");
+ zip.extract("install.rdf", dir);
+ dir = dir.parent;
+ dir.append("dictionaries");
+ dir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+ dir.append("ab-CD.dic");
+ zip.extract("dictionaries/ab-CD.dic", dir);
+ zip.close();
+
+ startupManager();
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1) {
+ // Should have installed and started
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+
+ // From run_test_21
+ dir = userExtDir.clone();
+ dir.append("ab-CD@dictionaries.addons.mozilla.org");
+ dir.remove(true);
+
+ restartManager();
+
+ run_test_23();
+ }));
+}
+
+// Tests that installing from a URL doesn't require a restart
+function run_test_23() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_dictionary.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], function() {
+ do_check_eq(install.type, "dictionary");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Dictionary");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_false(install.addon.hasResource("bootstrap.js"));
+ do_check_eq(install.addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL, 0);
+ do_check_not_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ let addon = install.addon;
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_check_true(addon.hasResource("install.rdf"));
+ // spin to let the addon startup finish
+ do_execute_soon(check_test_23);
+ });
+ });
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_23() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_false(b1.hasResource("bootstrap.js"));
+ do_check_in_crash_annotation("ab-CD@dictionaries.addons.mozilla.org", "1.0");
+
+ let dir = do_get_addon_root_uri(profileDir, "ab-CD@dictionaries.addons.mozilla.org");
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(list) {
+ do_check_eq(list.length, 0);
+
+ restartManager();
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1_2) {
+ b1_2.uninstall();
+ do_execute_soon(run_test_25);
+ });
+ }));
+ });
+ });
+}
+
+// Tests that updating from a bootstrappable add-on to a normal add-on calls
+// the uninstall method
+function run_test_25() {
+ restartManager();
+
+ HunspellEngine.listener = function(aEvent) {
+ HunspellEngine.listener = null;
+ do_check_eq(aEvent, "addDirectory");
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ installAllFiles([do_get_addon("test_dictionary_2")], function test_25_installed2() {
+ // Needs a restart to complete this so the old version stays running
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_true(b1.isActive);
+ do_check_true(hasFlag(b1.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ restartManager();
+
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "2.0");
+ do_check_true(b1_2.isActive);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_execute_soon(run_test_26);
+ });
+ }));
+ });
+ };
+
+ installAllFiles([do_get_addon("test_dictionary")], function test_25_installed() { });
+}
+
+// Tests that updating from a normal add-on to a bootstrappable add-on calls
+// the install method
+function run_test_26() {
+ installAllFiles([do_get_addon("test_dictionary")], function test_26_install() {
+ // Needs a restart to complete this
+ do_check_false(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org",
+ callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_true(b1.isActive);
+ do_check_true(hasFlag(b1.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ restartManager();
+
+ do_check_true(HunspellEngine.isDictionaryEnabled("ab-CD.dic"));
+
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1_2) {
+ do_check_neq(b1_2, null);
+ do_check_eq(b1_2.version, "1.0");
+ do_check_true(b1_2.isActive);
+ do_check_eq(b1_2.pendingOperations, AddonManager.PENDING_NONE);
+
+ HunspellEngine.deactivate();
+ b1_2.uninstall();
+ do_execute_soon(run_test_27);
+ });
+ }));
+ });
+}
+
+// Tests that an update check from a normal add-on to a bootstrappable add-on works
+function run_test_27() {
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "ab-CD@dictionaries.addons.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_dictionary.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Dictionary",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({
+ "ab-CD@dictionaries.addons.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], callback_soon(check_test_27));
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+}
+
+function check_test_27(install) {
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ restartManager();
+ AddonManager.getAddonByID("ab-CD@dictionaries.addons.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "2.0");
+ do_check_eq(b1.type, "dictionary");
+ b1.uninstall();
+ do_execute_soon(run_test_28);
+ });
+}
+
+// Tests that an update check from a bootstrappable add-on to a normal add-on works
+function run_test_28() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "ef@dictionaries.addons.mozilla.org",
+ version: "1.0",
+ type: "64",
+ updateURL: "http://localhost:" + gPort + "/data/test_dictionary.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Dictionary ef",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({
+ "ef@dictionaries.addons.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], callback_soon(check_test_28));
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+}
+
+function check_test_28(install) {
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ restartManager();
+ AddonManager.getAddonByID("ef@dictionaries.addons.mozilla.org", function(b2) {
+ do_check_neq(b2, null);
+ do_check_eq(b2.version, "2.0");
+ do_check_eq(b2.type, "extension");
+ b2.uninstall();
+ do_execute_soon(run_test_29);
+ });
+}
+
+// Tests that an update check from a bootstrappable add-on to a bootstrappable add-on works
+function run_test_29() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "gh@dictionaries.addons.mozilla.org",
+ version: "1.0",
+ type: "64",
+ updateURL: "http://localhost:" + gPort + "/data/test_dictionary.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Dictionary gh",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({
+ "gh@dictionaries.addons.mozilla.org": [
+ ["onInstalling", false /* = no restart */],
+ ["onInstalled", false]
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], check_test_29);
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+}
+
+function check_test_29(install) {
+ AddonManager.getAddonByID("gh@dictionaries.addons.mozilla.org", function(b2) {
+ do_check_neq(b2, null);
+ do_check_eq(b2.version, "2.0");
+ do_check_eq(b2.type, "dictionary");
+
+ prepare_test({
+ "gh@dictionaries.addons.mozilla.org": [
+ ["onUninstalling", false],
+ ["onUninstalled", false],
+ ]
+ }, [
+ ], callback_soon(finish_test_29));
+
+ b2.uninstall();
+ });
+}
+
+function finish_test_29() {
+ testserver.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_disable.js b/toolkit/mozapps/extensions/test/xpcshell/test_disable.js
new file mode 100644
index 000000000..867715863
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_disable.js
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+// This verifies that add-ons can be disabled and enabled.
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ optionsURL: "chrome://foo/content/options.xul",
+ aboutURL: "chrome://foo/content/about.xul",
+ iconURL: "chrome://foo/content/icon.png",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gIconURL = null;
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_eq(a1, null);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ writeInstallRDFForExtension(addon1, profileDir, addon1.id, "icon.png");
+ gIconURL = do_get_addon_root_uri(profileDir.clone(), addon1.id) + "icon.png";
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_neq(newa1, null);
+ do_check_true(newa1.isActive);
+ do_check_false(newa1.userDisabled);
+ do_check_eq(newa1.aboutURL, "chrome://foo/content/about.xul");
+ do_check_eq(newa1.optionsURL, "chrome://foo/content/options.xul");
+ do_check_eq(newa1.iconURL, "chrome://foo/content/icon.png");
+ do_check_true(isExtensionInAddonsList(profileDir, newa1.id));
+ do_check_true(hasFlag(newa1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(newa1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(newa1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ run_test_1();
+ });
+ }));
+}
+
+// Disabling an add-on should work
+function run_test_1() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_DISABLE, 0);
+ a1.userDisabled = true;
+ do_check_eq(a1.aboutURL, "chrome://foo/content/about.xul");
+ do_check_eq(a1.optionsURL, "chrome://foo/content/options.xul");
+ do_check_eq(a1.iconURL, "chrome://foo/content/icon.png");
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(a1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(list) {
+ do_check_eq(list.length, 1);
+ do_check_eq(list[0].id, "addon1@tests.mozilla.org");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_neq(newa1, null);
+ do_check_false(newa1.isActive);
+ do_check_true(newa1.userDisabled);
+ do_check_eq(newa1.aboutURL, null);
+ do_check_eq(newa1.optionsURL, null);
+ do_check_eq(newa1.iconURL, gIconURL);
+ do_check_false(isExtensionInAddonsList(profileDir, newa1.id));
+ do_check_false(hasFlag(newa1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(newa1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(newa1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ run_test_2();
+ });
+ }));
+ });
+}
+
+// Enabling an add-on should work.
+function run_test_2() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onEnabling"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ a1.userDisabled = false;
+ do_check_eq(a1.aboutURL, null);
+ do_check_eq(a1.optionsURL, null);
+ do_check_eq(a1.iconURL, gIconURL);
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(a1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(list) {
+ do_check_eq(list.length, 1);
+ do_check_eq(list[0].id, "addon1@tests.mozilla.org");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_neq(newa1, null);
+ do_check_true(newa1.isActive);
+ do_check_false(newa1.userDisabled);
+ do_check_eq(newa1.aboutURL, "chrome://foo/content/about.xul");
+ do_check_eq(newa1.optionsURL, "chrome://foo/content/options.xul");
+ do_check_eq(newa1.iconURL, "chrome://foo/content/icon.png");
+ do_check_true(isExtensionInAddonsList(profileDir, newa1.id));
+ do_check_true(hasFlag(newa1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(newa1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(newa1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE |
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ run_test_3();
+ });
+ }));
+ });
+}
+
+// Disabling then enabling without restart should fire onOperationCancelled.
+function run_test_3() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ a1.userDisabled = true;
+ ensure_test_completed();
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.userDisabled = false;
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_neq(newa1, null);
+ do_check_true(newa1.isActive);
+ do_check_false(newa1.userDisabled);
+ do_check_eq(newa1.aboutURL, "chrome://foo/content/about.xul");
+ do_check_eq(newa1.optionsURL, "chrome://foo/content/options.xul");
+ do_check_eq(newa1.iconURL, "chrome://foo/content/icon.png");
+ do_check_true(isExtensionInAddonsList(profileDir, newa1.id));
+ do_check_true(hasFlag(newa1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(newa1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js b/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js
new file mode 100644
index 000000000..720b454cc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_distribution.js
@@ -0,0 +1,273 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons distributed with the application get installed
+// correctly
+
+// Allow distributed add-ons to install
+Services.prefs.setBoolPref("extensions.installDistroAddons", true);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const distroDir = gProfD.clone();
+distroDir.append("distribution");
+distroDir.append("extensions");
+registerDirectory("XREAppDist", distroDir.parent);
+
+var addon1_1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test version 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "5"
+ }]
+};
+
+var addon1_2 = {
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test version 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "5"
+ }]
+};
+
+var addon1_3 = {
+ id: "addon1@tests.mozilla.org",
+ version: "3.0",
+ name: "Test version 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "5"
+ }]
+};
+
+function getActiveVersion() {
+ return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function getInstalledVersion() {
+ return Services.prefs.getIntPref("bootstraptest.installed_version");
+}
+
+function setOldModificationTime() {
+ // Make sure the installed extension has an old modification time so any
+ // changes will be detected
+ shutdownManager()
+ let extension = gProfD.clone();
+ extension.append("extensions");
+ if (Services.prefs.getBoolPref("extensions.alwaysUnpack"))
+ extension.append("addon1@tests.mozilla.org");
+ else
+ extension.append("addon1@tests.mozilla.org.xpi");
+ setExtensionModifiedTime(extension, Date.now() - MAKE_FILE_OLD_DIFFERENCE);
+ startupManager(false);
+}
+
+function run_test() {
+ do_test_pending();
+
+ run_test_1();
+}
+
+// Tests that on the first startup the add-on gets installed, with now as the
+// profile modifiedTime.
+function run_test_1() {
+ let extension = writeInstallRDFForExtension(addon1_1, distroDir);
+ setExtensionModifiedTime(extension, Date.now() - MAKE_FILE_OLD_DIFFERENCE);
+
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_false(a1.foreignInstall);
+
+ // Modification time should be updated when the addon is copied to the
+ // profile.
+ let testURI = a1.getResourceURI(TEST_UNPACKED ? "install.rdf" : "");
+ let testFile = testURI.QueryInterface(Components.interfaces.nsIFileURL).file;
+
+ do_check_true(testFile.exists());
+ let difference = testFile.lastModifiedTime - Date.now();
+ do_check_true(Math.abs(difference) < MAX_TIME_DIFFERENCE);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Tests that starting with a newer version in the distribution dir doesn't
+// install it yet
+function run_test_2() {
+ setOldModificationTime();
+
+ writeInstallRDFForExtension(addon1_2, distroDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Test that an app upgrade installs the newer version
+function run_test_3() {
+ restartManager("2");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_false(a1.foreignInstall);
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Test that an app upgrade doesn't downgrade the extension
+function run_test_4() {
+ setOldModificationTime();
+
+ writeInstallRDFForExtension(addon1_1, distroDir);
+
+ restartManager("3");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ do_execute_soon(run_test_5);
+ });
+}
+
+// Tests that after uninstalling a restart doesn't re-install the extension
+function run_test_5() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ a1.uninstall();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_2) {
+ do_check_eq(a1_2, null);
+
+ do_execute_soon(run_test_6);
+ });
+ }));
+}
+
+// Tests that upgrading the application still doesn't re-install the uninstalled
+// extension
+function run_test_6() {
+ restartManager("4");
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_eq(a1, null);
+
+ do_execute_soon(run_test_7);
+ });
+}
+
+// Tests that a pending install of a newer version of a distributed add-on
+// at app change still gets applied
+function run_test_7() {
+ Services.prefs.clearUserPref("extensions.installedDistroAddon.addon1@tests.mozilla.org");
+
+ installAllFiles([do_get_addon("test_distribution1_2")], function() {
+ restartManager(2);
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ a1.uninstall();
+ do_execute_soon(run_test_8);
+ });
+ });
+}
+
+// Tests that a pending install of a older version of a distributed add-on
+// at app change gets replaced by the distributed version
+function run_test_8() {
+ restartManager();
+
+ writeInstallRDFForExtension(addon1_3, distroDir);
+
+ installAllFiles([do_get_addon("test_distribution1_2")], function() {
+ restartManager(3);
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "3.0");
+ do_check_true(a1.isActive);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ a1.uninstall();
+ do_execute_soon(run_test_9);
+ });
+ });
+}
+
+// Tests that bootstrapped add-ons distributed start up correctly, also that
+// add-ons with multiple directories get copied fully
+function run_test_9() {
+ restartManager();
+
+ // Copy the test add-on to the distro dir
+ let addon = do_get_file("data/test_distribution2_2");
+ addon.copyTo(distroDir, "addon2@tests.mozilla.org");
+
+ restartManager("5");
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+
+ do_check_eq(getInstalledVersion(), 2);
+ do_check_eq(getActiveVersion(), 2);
+
+ do_check_true(a2.hasResource("bootstrap.js"));
+ do_check_true(a2.hasResource("subdir/dummy.txt"));
+ do_check_true(a2.hasResource("subdir/subdir2/dummy2.txt"));
+
+ // Currently installs are unpacked if the source is a directory regardless
+ // of the install.rdf property or the global preference
+
+ let addonDir = profileDir.clone();
+ addonDir.append("addon2@tests.mozilla.org");
+ do_check_true(addonDir.exists());
+ do_check_true(addonDir.isDirectory());
+ addonDir.append("subdir");
+ do_check_true(addonDir.exists());
+ do_check_true(addonDir.isDirectory());
+ addonDir.append("subdir2");
+ do_check_true(addonDir.exists());
+ do_check_true(addonDir.isDirectory());
+ addonDir.append("dummy2.txt");
+ do_check_true(addonDir.exists());
+ do_check_true(addonDir.isFile());
+
+ a2.uninstall();
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_dss.js b/toolkit/mozapps/extensions/test/xpcshell/test_dss.js
new file mode 100644
index 000000000..b408cc9c7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_dss.js
@@ -0,0 +1,824 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+// using a dynamic port in the addon metadata
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+
+// This verifies that themes behave as expected
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+const PREF_EXTENSIONS_DSS_ENABLED = "extensions.dss.enabled";
+
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Observer to ensure a "lightweight-theme-styling-update" notification is sent
+// when expected
+var gLWThemeChanged = false;
+var LightweightThemeObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "lightweight-theme-styling-update")
+ return;
+
+ gLWThemeChanged = true;
+ }
+};
+
+AM_Cc["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .addObserver(LightweightThemeObserver, "lightweight-theme-styling-update", false);
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, "theme1/1.0");
+ Services.prefs.setBoolPref(PREF_EXTENSIONS_DSS_ENABLED, true);
+ writeInstallRDFForExtension({
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ type: 4,
+ internalName: "theme1/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ // We need a default theme for some of these things to work but we have hidden
+ // the one in the application directory.
+ writeInstallRDFForExtension({
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Default",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ startupManager();
+ // Make sure we only register once despite multiple calls
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ do_check_neq(t1, null);
+ do_check_false(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_true(t1.isActive);
+ do_check_eq(t1.screenshots, null);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_false(t2.isActive);
+ do_check_eq(t2.screenshots, null);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_execute_soon(run_test_1);
+ });
+}
+
+function end_test() {
+ do_execute_soon(do_test_finished);
+}
+
+// Checks enabling one theme disables the others
+function run_test_1() {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_true(t1.userDisabled);
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_execute_soon(check_test_1);
+ });
+}
+
+function check_test_1() {
+ restartManager();
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme2/1.0");
+
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_false(t1.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_neq(t2, null);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_true(t2.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Removing the active theme should fall back to the default (not ideal in this
+// case since we don't have the default theme installed)
+function run_test_2() {
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("theme2@tests.mozilla.org"));
+ dest.remove(true);
+
+ restartManager();
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_false(t1.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(t2, null);
+ do_check_false(isThemeInAddonsList(profileDir, "theme2@tests.mozilla.org"));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Installing a lightweight theme should happen instantly and disable the default theme
+function run_test_3() {
+ writeInstallRDFForExtension({
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+ restartManager();
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled",
+ ["onEnabling", false],
+ "onEnabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled",
+ ]
+ }, [
+ "onExternalInstall"
+ ]);
+
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon.png"
+ };
+
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(null, p1);
+ do_check_eq(p1.name, "Test LW Theme");
+ do_check_eq(p1.version, "1");
+ do_check_eq(p1.type, "theme");
+ do_check_eq(p1.description, "A test theme");
+ do_check_eq(p1.creator, "Mozilla");
+ do_check_eq(p1.homepageURL, "http://localhost:" + gPort + "/data/index.html");
+ do_check_eq(p1.iconURL, "http://localhost:" + gPort + "/data/icon.png");
+ do_check_eq(p1.screenshots.length, 1);
+ do_check_eq(p1.screenshots[0], "http://localhost:" + gPort + "/data/preview.png");
+ do_check_false(p1.appDisabled);
+ do_check_false(p1.userDisabled);
+ do_check_true(p1.isCompatible);
+ do_check_true(p1.providesUpdatesSecurely);
+ do_check_eq(p1.blocklistState, 0);
+ do_check_true(p1.isActive);
+ do_check_eq(p1.pendingOperations, 0);
+ do_check_eq(p1.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE);
+ do_check_eq(p1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_true("isCompatibleWith" in p1);
+ do_check_true("findUpdates" in p1);
+
+ AddonManager.getAddonsByTypes(["theme"], function(addons) {
+ let seen = false;
+ addons.forEach(function(a) {
+ if (a.id == "1@personas.mozilla.org") {
+ seen = true;
+ }
+ else {
+ dump("Checking theme " + a.id + "\n");
+ do_check_false(a.isActive);
+ do_check_true(a.userDisabled);
+ }
+ });
+ do_check_true(seen);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_4);
+ });
+ });
+}
+
+// Installing a second lightweight theme should disable the first with no restart
+function run_test_4() {
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled",
+ ],
+ "2@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled",
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ }, [
+ "onExternalInstall"
+ ]);
+
+ LightweightThemeManager.currentTheme = {
+ id: "2",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A second test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon.png"
+ };
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByIDs(["1@personas.mozilla.org",
+ "2@personas.mozilla.org"], function([p1, p2]) {
+ do_check_neq(null, p2);
+ do_check_false(p2.appDisabled);
+ do_check_false(p2.userDisabled);
+ do_check_true(p2.isActive);
+ do_check_eq(p2.pendingOperations, 0);
+ do_check_eq(p2.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE);
+
+ do_check_neq(null, p1);
+ do_check_false(p1.appDisabled);
+ do_check_true(p1.userDisabled);
+ do_check_false(p1.isActive);
+ do_check_eq(p1.pendingOperations, 0);
+ do_check_eq(p1.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_ENABLE);
+
+ AddonManager.getAddonsByTypes(["theme"], function(addons) {
+ let seen = false;
+ addons.forEach(function(a) {
+ if (a.id == "2@personas.mozilla.org") {
+ seen = true;
+ }
+ else {
+ dump("Checking theme " + a.id + "\n");
+ do_check_false(a.isActive);
+ do_check_true(a.userDisabled);
+ }
+ });
+ do_check_true(seen);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_5);
+ });
+ });
+}
+
+// Switching to a custom theme should disable the lightweight theme and require
+// a restart. Cancelling that should also be possible.
+function run_test_5() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onEnabling"
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ ["onOperationCancelled", true]
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_ENABLE, t2.pendingOperations));
+ do_check_false(p2.isActive);
+ do_check_true(p2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, p2.pendingOperations));
+ do_check_true(hasFlag(AddonManager.PERM_CAN_ENABLE, p2.permissions));
+ do_check_true(gLWThemeChanged);
+
+ do_execute_soon(check_test_5);
+ });
+}
+
+function check_test_5() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_ENABLE, t2.pendingOperations));
+ do_check_false(p2.isActive);
+ do_check_true(p2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, p2.pendingOperations));
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+// Switching from a custom theme to a lightweight theme should require a restart
+function run_test_6() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onEnabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onOperationCancelled",
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onEnabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ do_check_false(p2.isActive);
+ do_check_false(p2.userDisabled);
+ do_check_true(hasFlag(AddonManager.PENDING_ENABLE, p2.pendingOperations));
+ do_check_false(t2.isActive);
+ do_check_true(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, t2.pendingOperations));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(check_test_6);
+ });
+}
+
+function check_test_6() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ do_check_true(p2.isActive);
+ do_check_false(p2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_ENABLE, p2.pendingOperations));
+ do_check_false(t2.isActive);
+ do_check_true(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, t2.pendingOperations));
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_7);
+ });
+}
+
+// Uninstalling a lightweight theme should not require a restart
+function run_test_7() {
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ p1.uninstall();
+
+ ensure_test_completed();
+ do_check_eq(LightweightThemeManager.usedThemes.length, 1);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Uninstalling a lightweight theme in use should not require a restart and it
+// should reactivate the default theme
+// Also, uninstalling a lightweight theme in use should send a
+// "lightweight-theme-styling-update" notification through the observer service
+function run_test_8() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ AddonManager.getAddonByID("2@personas.mozilla.org", function(p2) {
+ p2.uninstall();
+
+ ensure_test_completed();
+ do_check_eq(LightweightThemeManager.usedThemes.length, 0);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_9);
+ });
+}
+
+// Uninstalling a theme not in use should not require a restart
+function run_test_9() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ t1.uninstall();
+
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(newt1) {
+ do_check_eq(newt1, null);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_10);
+ });
+ });
+}
+
+// Uninstalling a custom theme in use should require a restart
+function run_test_10() {
+ AddonManager.getAddonByID("theme2@tests.mozilla.org", callback_soon(function(oldt2) {
+ prepare_test({
+ "theme2@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ oldt2.userDisabled = false;
+
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ callback_soon(function([d, t2]) {
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+
+ prepare_test({
+ "theme2@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ t2.uninstall();
+
+ ensure_test_completed();
+ do_check_false(gLWThemeChanged);
+
+ restartManager();
+
+ do_execute_soon(run_test_11);
+ }));
+ }));
+}
+
+// Installing a custom theme not in use should not require a restart
+function run_test_11() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_11));
+ install.install();
+ });
+}
+
+function check_test_11() {
+ restartManager();
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ do_check_neq(t1, null);
+ var previewSpec = do_get_addon_root_uri(profileDir, "theme1@tests.mozilla.org") + "preview.png";
+ do_check_eq(t1.screenshots.length, 1);
+ do_check_eq(t1.screenshots[0], previewSpec);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+// Updating a custom theme not in use should not require a restart
+function run_test_12() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_12);
+ install.install();
+ });
+}
+
+function check_test_12() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ do_check_neq(t1, null);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_13);
+ });
+}
+
+// Updating a custom theme in use should require a restart
+function run_test_13() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ t1.userDisabled = false;
+ ensure_test_completed();
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onInstalling",
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_13));
+ install.install();
+ });
+ }));
+}
+
+function check_test_13() {
+ restartManager();
+
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(gLWThemeChanged);
+ t1.uninstall();
+
+ do_execute_soon(run_test_14);
+ });
+}
+
+// Switching from a lightweight theme to the default theme should not require
+// a restart
+function run_test_14() {
+ restartManager();
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon.png"
+ };
+
+ AddonManager.getAddonByID("default@tests.mozilla.org", function(d) {
+ do_check_true(d.userDisabled);
+ do_check_false(d.isActive);
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ d.userDisabled = false;
+ ensure_test_completed();
+
+ do_check_false(d.userDisabled);
+ do_check_true(d.isActive);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_duplicateplugins.js b/toolkit/mozapps/extensions/test/xpcshell/test_duplicateplugins.js
new file mode 100644
index 000000000..4d1848ea4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_duplicateplugins.js
@@ -0,0 +1,187 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+var Ci = Components.interfaces;
+
+// This verifies that duplicate plugins are coalesced and maintain their ID
+// across restarts.
+
+var PLUGINS = [{
+ name: "Duplicate Plugin 1",
+ description: "A duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/home/mozilla/.plugins/dupplugin1.so"
+}, {
+ name: "Duplicate Plugin 1",
+ description: "A duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/usr/lib/plugins/dupplugin1.so"
+}, {
+ name: "Duplicate Plugin 2",
+ description: "Another duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/home/mozilla/.plugins/dupplugin2.so"
+}, {
+ name: "Duplicate Plugin 2",
+ description: "Another duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/usr/lib/plugins/dupplugin2.so"
+}, {
+ name: "Non-duplicate Plugin", // 3
+ description: "Not a duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/home/mozilla/.plugins/dupplugin3.so"
+}, {
+ name: "Non-duplicate Plugin", // 4
+ description: "Not a duplicate because the descriptions are different",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/usr/lib/plugins/dupplugin4.so"
+}, {
+ name: "Another Non-duplicate Plugin", // 5
+ description: "Not a duplicate plugin",
+ version: "1",
+ blocklisted: false,
+ enabledState: Ci.nsIPluginTag.STATE_ENABLED,
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ },
+ filename: "/home/mozilla/.plugins/dupplugin5.so"
+}];
+
+// A fake plugin host to return the plugins defined above
+var PluginHost = {
+ getPluginTags: function(countRef) {
+ countRef.value = PLUGINS.length;
+ return PLUGINS;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Components.interfaces.nsIPluginHost)
+ || iid.equals(Components.interfaces.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/plugin/host;1", PluginHost);
+
+var gPluginIDs = [null, null, null, null, null];
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ Services.prefs.setBoolPref("media.gmp-provider.enabled", false);
+
+ startupManager();
+
+ run_test_1();
+}
+
+function found_plugin(aNum, aId) {
+ if (gPluginIDs[aNum])
+ do_throw("Found duplicate of plugin " + aNum);
+ gPluginIDs[aNum] = aId;
+}
+
+// Test that the plugins were coalesced and all appear in the returned list
+function run_test_1() {
+ AddonManager.getAddonsByTypes(["plugin"], function(aAddons) {
+ do_check_eq(aAddons.length, 5);
+ aAddons.forEach(function(aAddon) {
+ if (aAddon.name == "Duplicate Plugin 1") {
+ found_plugin(0, aAddon.id);
+ do_check_eq(aAddon.description, "A duplicate plugin");
+ }
+ else if (aAddon.name == "Duplicate Plugin 2") {
+ found_plugin(1, aAddon.id);
+ do_check_eq(aAddon.description, "Another duplicate plugin");
+ }
+ else if (aAddon.name == "Another Non-duplicate Plugin") {
+ found_plugin(5, aAddon.id);
+ do_check_eq(aAddon.description, "Not a duplicate plugin");
+ }
+ else if (aAddon.name == "Non-duplicate Plugin") {
+ if (aAddon.description == "Not a duplicate plugin")
+ found_plugin(3, aAddon.id);
+ else if (aAddon.description == "Not a duplicate because the descriptions are different")
+ found_plugin(4, aAddon.id);
+ else
+ do_throw("Found unexpected plugin with description " + aAddon.description);
+ }
+ else {
+ do_throw("Found unexpected plugin " + aAddon.name);
+ }
+ });
+
+ run_test_2();
+ });
+}
+
+// Test that disabling a coalesced plugin disables all its tags
+function run_test_2() {
+ AddonManager.getAddonByID(gPluginIDs[0], function(p) {
+ do_check_false(p.userDisabled);
+ p.userDisabled = true;
+ do_check_true(PLUGINS[0].disabled);
+ do_check_true(PLUGINS[1].disabled);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Test that IDs persist across restart
+function run_test_3() {
+ restartManager();
+
+ AddonManager.getAddonByID(gPluginIDs[0], callback_soon(function(p) {
+ do_check_neq(p, null);
+ do_check_eq(p.name, "Duplicate Plugin 1");
+ do_check_eq(p.description, "A duplicate plugin");
+
+ // Reorder the plugins and restart again
+ [PLUGINS[0], PLUGINS[1]] = [PLUGINS[1], PLUGINS[0]];
+ restartManager();
+
+ AddonManager.getAddonByID(gPluginIDs[0], function(p_2) {
+ do_check_neq(p_2, null);
+ do_check_eq(p_2.name, "Duplicate Plugin 1");
+ do_check_eq(p_2.description, "A duplicate plugin");
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_e10s_restartless.js b/toolkit/mozapps/extensions/test/xpcshell/test_e10s_restartless.js
new file mode 100644
index 000000000..1a3a0e747
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_e10s_restartless.js
@@ -0,0 +1,429 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+const ID2 = "bootstrap2@tests.mozilla.org";
+
+const APP_STARTUP = 1;
+const ADDON_INSTALL = 5;
+
+function getStartupReason(id) {
+ let info = BootstrapMonitor.started.get(id);
+ return info ? info.reason : undefined;
+}
+
+BootstrapMonitor.init();
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+startupManager();
+
+function* check_normal() {
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, install.addon);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE));
+ addon.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_false(hasFlag(addon.pendingOperations, AddonManager.PENDING_DISABLE));
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE));
+ addon.userDisabled = false;
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_true(addon.isActive);
+ do_check_false(hasFlag(addon.pendingOperations, AddonManager.PENDING_ENABLE));
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL));
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ yield promiseRestartManager();
+}
+
+// Installing the add-on normally doesn't require a restart
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = false;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", false);
+
+ yield check_normal();
+});
+
+// Enabling the pref doesn't change anything
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = false;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+
+ yield check_normal();
+});
+
+// Default e10s doesn't change anything
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", false);
+
+ yield check_normal();
+});
+
+// Pref and e10s blocks install
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_true(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ yield promiseRestartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE));
+ addon.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_false(hasFlag(addon.pendingOperations, AddonManager.PENDING_DISABLE));
+
+ do_check_true(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE));
+ addon.userDisabled = false;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_true(hasFlag(addon.pendingOperations, AddonManager.PENDING_ENABLE));
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ do_check_true(addon.isActive);
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL));
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ yield promiseRestartManager();
+});
+
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_true(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ yield promiseRestartManager();
+
+ // After install and restart we should block.
+ let blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE));
+ addon.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_false(hasFlag(addon.pendingOperations, AddonManager.PENDING_DISABLE));
+ do_check_true(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE));
+
+ yield promiseRestartManager();
+
+ // After disable and restart we should not block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+ addon = yield promiseAddonByID(ID);
+ addon.userDisabled = false;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_true(hasFlag(addon.pendingOperations, AddonManager.PENDING_ENABLE));
+
+ yield promiseRestartManager();
+
+ // After re-enable and restart we should block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ do_check_true(addon.isActive);
+ BootstrapMonitor.checkAddonStarted(ID);
+ // This should probably be ADDON_ENABLE, but its not easy to make
+ // that happen. See bug 1304392 for discussion.
+ do_check_eq(getStartupReason(ID), APP_STARTUP);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL));
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ yield promiseRestartManager();
+
+ // After uninstall and restart we should not block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+ restartManager();
+});
+
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+
+ let install1 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ let install2 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap2_1"), resolve));
+ yield promiseCompleteAllInstalls([install1, install2]);
+ do_check_eq(install1.state, AddonManager.STATE_INSTALLED);
+ do_check_eq(install2.state, AddonManager.STATE_INSTALLED);
+ do_check_true(hasFlag(install1.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+ do_check_true(hasFlag(install2.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ let addon = yield promiseAddonByID(ID);
+ let addon2 = yield promiseAddonByID(ID2);
+
+ do_check_eq(addon, null);
+ do_check_eq(addon2, null);
+
+ yield promiseRestartManager();
+
+ // After install and restart we should block.
+ let blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+
+ BootstrapMonitor.checkAddonInstalled(ID2);
+ BootstrapMonitor.checkAddonStarted(ID2);
+ do_check_eq(getStartupReason(ID2), ADDON_INSTALL);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ addon2 = yield promiseAddonByID(ID2);
+ do_check_neq(addon2, null);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE));
+ addon.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_false(hasFlag(addon.pendingOperations, AddonManager.PENDING_DISABLE));
+ do_check_true(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE));
+
+ yield promiseRestartManager();
+
+ // After disable one addon and restart we should block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ addon2 = yield promiseAddonByID(ID2);
+
+ do_check_false(hasFlag(addon2.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE));
+ addon2.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID2);
+ do_check_false(addon2.isActive);
+ do_check_false(hasFlag(addon2.pendingOperations, AddonManager.PENDING_DISABLE));
+ do_check_true(hasFlag(addon2.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE));
+
+ yield promiseRestartManager();
+
+ // After disable both addons and restart we should not block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+ addon = yield promiseAddonByID(ID);
+ addon.userDisabled = false;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_false(addon.isActive);
+ do_check_true(hasFlag(addon.pendingOperations, AddonManager.PENDING_ENABLE));
+
+ yield promiseRestartManager();
+
+ // After re-enable one addon and restart we should block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ do_check_true(addon.isActive);
+ BootstrapMonitor.checkAddonStarted(ID);
+ // Bug 1304392 again (see comment above)
+ do_check_eq(getStartupReason(ID), APP_STARTUP);
+
+ do_check_false(hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL));
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ yield promiseRestartManager();
+
+ // After uninstall the only enabled addon and restart we should not block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+ addon2 = yield promiseAddonByID(ID2);
+ addon2.uninstall();
+
+ restartManager();
+});
+
+// Check that the rollout policy sets work as expected
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+ Services.prefs.setCharPref("extensions.e10s.rollout.policy", "xpcshell-test");
+
+ // Both 'bootstrap1' and 'bootstrap2' addons are listed in the allowed policy
+ // set, so they should install and start normally.
+ yield check_normal();
+
+ // Check that the two add-ons can be installed together correctly as
+ // check_normal() only perform checks on bootstrap1.
+ let install1 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ let install2 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap2_1"), resolve));
+ yield promiseCompleteAllInstalls([install1, install2]);
+
+ do_check_eq(install1.state, AddonManager.STATE_INSTALLED);
+ do_check_eq(install2.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install1.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+ do_check_false(hasFlag(install2.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ let addon = yield promiseAddonByID(ID);
+ let addon2 = yield promiseAddonByID(ID2);
+
+ do_check_neq(addon, null);
+ do_check_neq(addon2, null);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID2);
+ BootstrapMonitor.checkAddonStarted(ID2);
+
+ yield promiseRestartManager();
+
+ // After install and restart e10s should not be blocked.
+ let blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+ // Check that adding bootstrap2 to the blocklist will trigger a disable of e10s.
+ Services.prefs.setCharPref("extensions.e10s.rollout.blocklist", ID2);
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ yield promiseRestartManager();
+
+ // Check that after restarting, e10s continues to be blocked.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ // Check that uninstalling bootstrap2 (which is in the blocklist) will
+ // cause e10s to be re-enabled.
+ addon2 = yield promiseAddonByID(ID2);
+ do_check_false(hasFlag(addon2.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL));
+ addon2.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID2);
+ BootstrapMonitor.checkAddonNotInstalled(ID2);
+
+ yield promiseRestartManager();
+
+ // After uninstall the blocklisted addon and restart we should not block.
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_false(blocked);
+
+
+ // Let's perform similar checks again, now that bootstrap2 is in the blocklist.
+ // The bootstrap1 add-on should install and start correctly, but bootstrap2 should not.
+ addon = yield promiseAddonByID(ID);
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ yield promiseRestartManager();
+
+ install1 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ install2 = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap2_1"), resolve));
+ yield promiseCompleteAllInstalls([install1, install2]);
+
+ do_check_eq(install1.state, AddonManager.STATE_INSTALLED);
+ do_check_eq(install2.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install1.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+ do_check_true(hasFlag(install2.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ addon = yield promiseAddonByID(ID);
+ addon2 = yield promiseAddonByID(ID2);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon2, null);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ BootstrapMonitor.checkAddonNotInstalled(ID2);
+ BootstrapMonitor.checkAddonNotStarted(ID2);
+
+ yield promiseRestartManager();
+
+ blocked = Services.prefs.getBoolPref("extensions.e10sBlockedByAddons");
+ do_check_true(blocked);
+
+ // Clean-up
+ addon = yield promiseAddonByID(ID);
+ addon2 = yield promiseAddonByID(ID2);
+
+ addon.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+
+ addon2.uninstall();
+ BootstrapMonitor.checkAddonNotStarted(ID2);
+ BootstrapMonitor.checkAddonNotInstalled(ID2);
+
+ Services.prefs.clearUserPref("extensions.e10s.rollout.policy");
+ Services.prefs.clearUserPref("extensions.e10s.rollout.blocklist");
+
+ yield promiseRestartManager();
+});
+
+// The hotfix is unaffected
+add_task(function*() {
+ gAppInfo.browserTabsRemoteAutostart = true;
+ Services.prefs.setBoolPref("extensions.e10sBlocksEnabling", true);
+ Services.prefs.setCharPref("extensions.hotfix.id", ID);
+ Services.prefs.setBoolPref("extensions.hotfix.cert.checkAttributes", false);
+
+ yield check_normal();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_error.js b/toolkit/mozapps/extensions/test/xpcshell/test_error.js
new file mode 100644
index 000000000..11a465b55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_error.js
@@ -0,0 +1,90 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that various error conditions are handled correctly
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+
+ do_test_pending();
+ run_test_1();
+}
+
+// Checks that a local file validates ok
+function run_test_1() {
+ AddonManager.getInstallForFile(do_get_file("data/unsigned.xpi"), function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.error, 0);
+
+ install.cancel();
+
+ run_test_2();
+ });
+}
+
+// Checks that a corrupt file shows an error
+function run_test_2() {
+ AddonManager.getInstallForFile(do_get_file("data/corrupt.xpi"), function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+
+ run_test_3();
+ });
+}
+
+// Checks that an empty file shows an error
+function run_test_3() {
+ AddonManager.getInstallForFile(do_get_file("data/empty.xpi"), function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+
+ run_test_4();
+ });
+}
+
+// Checks that a file that doesn't match its hash shows an error
+function run_test_4() {
+ let url = Services.io.newFileURI(do_get_file("data/unsigned.xpi")).spec;
+ AddonManager.getInstallForURL(url, function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_INCORRECT_HASH);
+
+ run_test_5();
+ }, "application/x-xpinstall", "sha1:foo");
+}
+
+// Checks that a file that doesn't exist shows an error
+function run_test_5() {
+ let file = do_get_file("data");
+ file.append("missing.xpi");
+ AddonManager.getInstallForFile(file, function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_NETWORK_FAILURE);
+
+ run_test_6();
+ });
+}
+
+// Checks that an add-on with an illegal ID shows an error
+function run_test_6() {
+ AddonManager.getInstallForFile(do_get_addon("test_bug567173"), function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js b/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js
new file mode 100644
index 000000000..3dcd83da8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_experiment.js
@@ -0,0 +1,131 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+var scope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+const XPIProvider = scope.XPIProvider;
+const ID = "experiment1@tests.mozilla.org";
+
+var gIsNightly = false;
+
+function run_test() {
+ BootstrapMonitor.init();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ gIsNightly = isNightlyChannel();
+
+ run_next_test();
+}
+
+add_task(function* test_experiment() {
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseInstallAllFiles([do_get_addon("test_experiment1")]);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ let addon = yield promiseAddonByID(ID);
+ Assert.ok(addon, "Addon is found.");
+
+ Assert.ok(addon.userDisabled, "Experiments are userDisabled by default.");
+ Assert.ok(!addon.appDisabled, "Experiments are not appDisabled by compatibility.");
+ Assert.equal(addon.isActive, false, "Add-on is not active.");
+ Assert.equal(addon.updateURL, null, "No updateURL for experiments.");
+ Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE,
+ "Background updates are disabled.");
+ Assert.equal(addon.permissions, AddonManager.PERM_CAN_UNINSTALL + AddonManager.PERM_CAN_ENABLE,
+ "Permissions are minimal.");
+ Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_ENABLE),
+ "Should not be pending enable");
+ Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_DISABLE),
+ "Should not be pending disable");
+
+ // Setting applyBackgroundUpdates should not work.
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE,
+ "Setting applyBackgroundUpdates shouldn't do anything.");
+
+ let noCompatibleCalled = false;
+ let noUpdateCalled = false;
+ let finishedCalled = false;
+
+ let listener = {
+ onNoCompatibilityUpdateAvailable: () => { noCompatibleCalled = true; },
+ onNoUpdateAvailable: () => { noUpdateCalled = true; },
+ onUpdateFinished: () => { finishedCalled = true; },
+ };
+
+ addon.findUpdates(listener, "testing", null, null);
+ Assert.ok(noCompatibleCalled, "Listener called.");
+ Assert.ok(noUpdateCalled, "Listener called.");
+ Assert.ok(finishedCalled, "Listener called.");
+});
+
+// Changes to userDisabled should not be persisted to the database.
+add_task(function* test_userDisabledNotPersisted() {
+ let addon = yield promiseAddonByID(ID);
+ Assert.ok(addon, "Add-on is found.");
+ Assert.ok(addon.userDisabled, "Add-on is user disabled.");
+
+ let promise = promiseAddonEvent("onEnabled");
+ addon.userDisabled = false;
+ let [addon2] = yield promise;
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ Assert.equal(addon2.id, addon.id, "Changed add-on matches expected.");
+ Assert.equal(addon2.userDisabled, false, "Add-on is no longer user disabled.");
+ Assert.ok(addon2.isActive, "Add-on is active.");
+
+ Assert.ok(ID in XPIProvider.bootstrappedAddons,
+ "Experiment add-on listed in XPIProvider bootstrapped list.");
+
+ addon = yield promiseAddonByID(ID);
+ Assert.ok(addon, "Add-on retrieved.");
+ Assert.equal(addon.userDisabled, false, "Add-on is still enabled after API retrieve.");
+ Assert.ok(addon.isActive, "Add-on is still active.");
+ Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_ENABLE),
+ "Should not be pending enable");
+ Assert.ok(!(addon.pendingOperations & AddonManager.PENDING_DISABLE),
+ "Should not be pending disable");
+
+ // Now when we restart the manager the add-on should revert state.
+ yield promiseRestartManager();
+ let persisted = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
+ Assert.ok(!(ID in persisted),
+ "Experiment add-on not persisted to bootstrappedAddons.");
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ addon = yield promiseAddonByID(ID);
+ Assert.ok(addon, "Add-on retrieved.");
+ Assert.ok(addon.userDisabled, "Add-on is disabled after restart.");
+ Assert.equal(addon.isActive, false, "Add-on is not active after restart.");
+ addon.uninstall();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+});
+
+add_task(function* test_checkCompatibility() {
+ if (gIsNightly)
+ Services.prefs.setBoolPref("extensions.checkCompatibility.nightly", false);
+ else
+ Services.prefs.setBoolPref("extensions.checkCompatibility.1", false);
+
+ yield promiseRestartManager();
+
+ yield promiseInstallAllFiles([do_get_addon("test_experiment1")]);
+ let addon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ Assert.ok(addon, "Add-on is found.");
+ Assert.ok(addon.userDisabled, "Add-on is user disabled.");
+ Assert.ok(!addon.appDisabled, "Add-on is not app disabled.");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
new file mode 100644
index 000000000..a3b3f477c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_ext_management.js
@@ -0,0 +1,137 @@
+"use strict";
+
+add_task(function* setup() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "48", "48");
+ startupManager();
+});
+
+/* eslint-disable no-undef */
+// Shared background function for getSelf tests
+function backgroundGetSelf() {
+ browser.management.getSelf().then(extInfo => {
+ browser.test.sendMessage("management-getSelf", extInfo);
+ }, error => {
+ browser.test.notifyFail(`getSelf rejected with error: ${error}`);
+ });
+}
+/* eslint-enable no-undef */
+
+add_task(function* test_management_get_self_complete() {
+ const id = "get_self_test_complete@tests.mozilla.com";
+ const permissions = ["management", "cookies"];
+ const hostPermissions = ["*://example.org/", "https://foo.example.org/"];
+
+ let manifest = {
+ applications: {
+ gecko: {
+ id,
+ update_url: "https://updates.mozilla.com/",
+ },
+ },
+ name: "test extension name",
+ short_name: "test extension short name",
+ description: "test extension description",
+ version: "1.0",
+ homepage_url: "http://www.example.com/",
+ options_ui: {
+ "page": "get_self_options.html",
+ },
+ icons: {
+ "16": "icons/icon-16.png",
+ "48": "icons/icon-48.png",
+ },
+ permissions: [...permissions, ...hostPermissions],
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background: backgroundGetSelf,
+ useAddonManager: "temporary",
+ });
+ yield extension.startup();
+ let extInfo = yield extension.awaitMessage("management-getSelf");
+
+ equal(extInfo.id, id, "getSelf returned the expected id");
+ equal(extInfo.installType, "development", "getSelf returned the expected installType");
+ for (let prop of ["name", "description", "version"]) {
+ equal(extInfo[prop], manifest[prop], `getSelf returned the expected ${prop}`);
+ }
+ equal(extInfo.shortName, manifest.short_name, "getSelf returned the expected shortName");
+ equal(extInfo.mayDisable, true, "getSelf returned the expected value for mayDisable");
+ equal(extInfo.enabled, true, "getSelf returned the expected value for enabled");
+ equal(extInfo.homepageUrl, manifest.homepage_url, "getSelf returned the expected homepageUrl");
+ equal(extInfo.updateUrl, manifest.applications.gecko.update_url, "getSelf returned the expected updateUrl");
+ ok(extInfo.optionsUrl.endsWith(manifest.options_ui.page), "getSelf returned the expected optionsUrl");
+ for (let [index, size] of Object.keys(manifest.icons).sort().entries()) {
+ equal(extInfo.icons[index].size, +size, "getSelf returned the expected icon size");
+ equal(extInfo.icons[index].url, manifest.icons[size], "getSelf returned the expected icon url");
+ }
+ deepEqual(extInfo.permissions.sort(), permissions.sort(), "getSelf returned the expected permissions");
+ deepEqual(extInfo.hostPermissions.sort(), hostPermissions.sort(), "getSelf returned the expected hostPermissions");
+ equal(extInfo.installType, "development", "getSelf returned the expected installType");
+ yield extension.unload();
+});
+
+add_task(function* test_management_get_self_minimal() {
+ const id = "get_self_test_minimal@tests.mozilla.com";
+
+ let manifest = {
+ applications: {
+ gecko: {
+ id,
+ },
+ },
+ name: "test extension name",
+ version: "1.0",
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background: backgroundGetSelf,
+ useAddonManager: "temporary",
+ });
+ yield extension.startup();
+ let extInfo = yield extension.awaitMessage("management-getSelf");
+
+ equal(extInfo.id, id, "getSelf returned the expected id");
+ equal(extInfo.installType, "development", "getSelf returned the expected installType");
+ for (let prop of ["name", "version"]) {
+ equal(extInfo[prop], manifest[prop], `getSelf returned the expected ${prop}`);
+ }
+ for (let prop of ["shortName", "description", "optionsUrl"]) {
+ equal(extInfo[prop], "", `getSelf returned the expected ${prop}`);
+ }
+ for (let prop of ["homepageUrl", " updateUrl", "icons"]) {
+ equal(Reflect.getOwnPropertyDescriptor(extInfo, prop), undefined, `getSelf did not return a ${prop} property`);
+ }
+ for (let prop of ["permissions", "hostPermissions"]) {
+ deepEqual(extInfo[prop], [], `getSelf returned the expected ${prop}`);
+ }
+ yield extension.unload();
+});
+
+add_task(function* test_management_get_self_permanent() {
+ const id = "get_self_test_permanent@tests.mozilla.com";
+
+ let manifest = {
+ applications: {
+ gecko: {
+ id,
+ },
+ },
+ name: "test extension name",
+ version: "1.0",
+ };
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest,
+ background: backgroundGetSelf,
+ useAddonManager: "permanent",
+ });
+ yield extension.startup();
+ let extInfo = yield extension.awaitMessage("management-getSelf");
+
+ equal(extInfo.id, id, "getSelf returned the expected id");
+ equal(extInfo.installType, "normal", "getSelf returned the expected installType");
+ yield extension.unload();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js b/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
new file mode 100644
index 000000000..406489e40
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_filepointer.js
@@ -0,0 +1,403 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that various operations with file pointers work and do not affect the
+// source files
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon1_2 = {
+ id: "addon1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+profileDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+const sourceDir = gProfD.clone();
+sourceDir.append("source");
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver;
+
+function writePointer(aId, aName) {
+ let file = profileDir.clone();
+ file.append(aName ? aName : aId);
+
+ let target = sourceDir.clone();
+ target.append(do_get_expected_addon_name(aId));
+
+ var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(AM_Ci.nsIFileOutputStream);
+ fos.init(file,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+ fos.write(target.path, target.path.length);
+ fos.close();
+}
+
+function writeRelativePointer(aId, aName) {
+ let file = profileDir.clone();
+ file.append(aName ? aName : aId);
+
+ let absTarget = sourceDir.clone();
+ absTarget.append(do_get_expected_addon_name(aId));
+
+ var relTarget = absTarget.getRelativeDescriptor(profileDir);
+
+ var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(AM_Ci.nsIFileOutputStream);
+ fos.init(file,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+ fos.write(relTarget, relTarget.length);
+ fos.close();
+}
+
+function run_test() {
+ // pointer files only work with unpacked directories
+ if (Services.prefs.getBoolPref("extensions.alwaysUnpack") == false)
+ return;
+
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+ testserver.start(-1);
+ gPort = testserver.identity.primaryPort;
+
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+// Tests that installing a new add-on by pointer works
+function run_test_1() {
+ writeInstallRDFForExtension(addon1, sourceDir);
+ writePointer(addon1.id);
+
+ startupManager();
+
+ AddonManager.getAddonByID(addon1.id, function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ let file = a1.getResourceURI().QueryInterface(AM_Ci.nsIFileURL).file;
+ do_check_eq(file.parent.path, sourceDir.path);
+
+ let rootUri = do_get_addon_root_uri(sourceDir, addon1.id);
+ let uri = a1.getResourceURI("/");
+ do_check_eq(uri.spec, rootUri);
+ uri = a1.getResourceURI("install.rdf");
+ do_check_eq(uri.spec, rootUri + "install.rdf");
+
+ // Check that upgrade is disabled for addons installed by file-pointers.
+ do_check_eq(a1.permissions & AddonManager.PERM_CAN_UPGRADE, 0);
+ run_test_2();
+ });
+}
+
+// Tests that installing the addon from some other source doesn't clobber
+// the original sources
+function run_test_2() {
+ prepare_test({}, [
+ "onNewInstall",
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_filepointer.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], callback_soon(check_test_2));
+
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_2() {
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+
+ let file = a1.getResourceURI().QueryInterface(AM_Ci.nsIFileURL).file;
+ do_check_eq(file.parent.path, profileDir.path);
+
+ let rootUri = do_get_addon_root_uri(profileDir, addon1.id);
+ let uri = a1.getResourceURI("/");
+ do_check_eq(uri.spec, rootUri);
+ uri = a1.getResourceURI("install.rdf");
+ do_check_eq(uri.spec, rootUri + "install.rdf");
+
+ let source = sourceDir.clone();
+ source.append(addon1.id);
+ do_check_true(source.exists());
+
+ a1.uninstall();
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Tests that uninstalling doesn't clobber the original sources
+function run_test_3() {
+ restartManager();
+
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ a1.uninstall();
+
+ restartManager();
+
+ let source = sourceDir.clone();
+ source.append(addon1.id);
+ do_check_true(source.exists());
+
+ do_execute_soon(run_test_4);
+ }));
+}
+
+// Tests that misnaming a pointer doesn't clobber the sources
+function run_test_4() {
+ writePointer("addon2@tests.mozilla.org", addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_eq(a1, null);
+ do_check_eq(a2, null);
+
+ let source = sourceDir.clone();
+ source.append(addon1.id);
+ do_check_true(source.exists());
+
+ let pointer = profileDir.clone();
+ pointer.append("addon2@tests.mozilla.org");
+ do_check_false(pointer.exists());
+
+ do_execute_soon(run_test_5);
+ });
+}
+
+// Tests that changing the ID of an existing add-on doesn't clobber the sources
+function run_test_5() {
+ var dest = writeInstallRDFForExtension(addon1, sourceDir);
+ // Make sure the modification time changes enough to be detected.
+ setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ writeInstallRDFForExtension(addon2, sourceDir, addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1_2, a2_2]) {
+ do_check_eq(a1_2, null);
+ do_check_eq(a2_2, null);
+
+ let source = sourceDir.clone();
+ source.append(addon1.id);
+ do_check_true(source.exists());
+
+ let pointer = profileDir.clone();
+ pointer.append(addon1.id);
+ do_check_false(pointer.exists());
+
+ do_execute_soon(run_test_6);
+ });
+ }));
+}
+
+// Removing the pointer file should uninstall the add-on
+function run_test_6() {
+ var dest = writeInstallRDFForExtension(addon1, sourceDir);
+ // Make sure the modification time changes enough to be detected in run_test_8.
+ setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ let pointer = profileDir.clone();
+ pointer.append(addon1.id);
+ pointer.remove(false);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_2) {
+ do_check_eq(a1_2, null);
+
+ do_execute_soon(run_test_7);
+ });
+ }));
+}
+
+// Removing the pointer file and replacing it with a directory should work
+function run_test_7() {
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ let pointer = profileDir.clone();
+ pointer.append(addon1.id);
+ pointer.remove(false);
+
+ writeInstallRDFForExtension(addon1_2, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_2) {
+ do_check_neq(a1_2, null);
+ do_check_eq(a1_2.version, "2.0");
+
+ a1_2.uninstall();
+
+ do_execute_soon(run_test_8);
+ });
+ }));
+}
+
+// Changes to the source files should be detected
+function run_test_8() {
+ restartManager();
+
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ writeInstallRDFForExtension(addon1_2, sourceDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1_2) {
+ do_check_neq(a1_2, null);
+ do_check_eq(a1_2.version, "2.0");
+
+ a1_2.uninstall();
+
+ do_execute_soon(run_test_9);
+ });
+ }));
+}
+
+// Removing the add-on the pointer file points at should uninstall the add-on
+function run_test_9() {
+ restartManager();
+
+ var dest = writeInstallRDFForExtension(addon1, sourceDir);
+ writePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ dest.remove(true);
+
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, function(a1_2) {
+ do_check_eq(a1_2, null);
+
+ let pointer = profileDir.clone();
+ pointer.append(addon1.id);
+ do_check_false(pointer.exists());
+
+ do_execute_soon(run_test_10);
+ });
+ }));
+}
+
+// Tests that installing a new add-on by pointer with a relative path works
+function run_test_10() {
+ writeInstallRDFForExtension(addon1, sourceDir);
+ writeRelativePointer(addon1.id);
+
+ restartManager();
+
+ AddonManager.getAddonByID(addon1.id, function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+
+ let file = a1.getResourceURI().QueryInterface(AM_Ci.nsIFileURL).file;
+ do_check_eq(file.parent.path, sourceDir.path);
+
+ let rootUri = do_get_addon_root_uri(sourceDir, addon1.id);
+ let uri = a1.getResourceURI("/");
+ do_check_eq(uri.spec, rootUri);
+ uri = a1.getResourceURI("install.rdf");
+ do_check_eq(uri.spec, rootUri + "install.rdf");
+
+ // Check that upgrade is disabled for addons installed by file-pointers.
+ do_check_eq(a1.permissions & AddonManager.PERM_CAN_UPGRADE, 0);
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_fuel.js b/toolkit/mozapps/extensions/test/xpcshell/test_fuel.js
new file mode 100644
index 000000000..0c3035f76
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_fuel.js
@@ -0,0 +1,165 @@
+/* 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 just verifies that FUEL integrates to the add-ons manager
+
+var testdata = {
+ dummyid: "fuel-dummy-extension@mozilla.org",
+ dummyname: "Dummy Extension",
+ inspectorid: "addon1@tests.mozilla.org",
+ inspectorname: "Test Addon",
+ missing: "fuel.fuel-test-missing",
+ dummy: "fuel.fuel-test"
+};
+
+var Application = null
+
+function run_test() {
+ var cm = AM_Cc["@mozilla.org/categorymanager;1"].
+ getService(AM_Ci.nsICategoryManager);
+
+ try {
+ var contract = cm.getCategoryEntry("JavaScript-global-privileged-property",
+ "Application");
+ Application = AM_Cc[contract].getService(AM_Ci.extIApplication);
+ }
+ catch (e) {
+ // This application does not include a FUEL variant.
+ return;
+ }
+
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ const profileDir = gProfD.clone();
+ profileDir.append("extensions");
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test Addon",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ }, profileDir);
+
+ startupManager();
+
+ Application.getExtensions(function(extensions) {
+ // test to see if the extensions object is available
+ do_check_neq(extensions, null);
+
+ // test to see if a non-existant extension exists
+ do_check_true(!extensions.has(testdata.dummyid));
+
+ // test to see if an extension exists
+ do_check_true(extensions.has(testdata.inspectorid));
+
+ var inspector = extensions.get(testdata.inspectorid);
+ do_check_eq(inspector.id, testdata.inspectorid);
+ do_check_eq(inspector.name, testdata.inspectorname);
+ do_check_eq(inspector.version, "1.0");
+ do_check_true(inspector.firstRun, true);
+ do_check_true(inspector.enabled);
+
+ // test to see if extension find works
+ do_check_eq(extensions.all.length, 1);
+ // STORAGE TESTING
+ // Make sure the we are given the same extension (cached) so things like .storage work right
+ inspector.storage.set("test", "simple check");
+ do_check_true(inspector.storage.has("test"));
+
+ var inspector2 = extensions.get(testdata.inspectorid);
+ do_check_eq(inspector2.id, testdata.inspectorid);
+ do_check_true(inspector.storage.has("test"));
+ do_check_eq(inspector2.storage.get("test", "cache"), inspector.storage.get("test", "original"));
+
+ inspector.events.addListener("disable", onGenericEvent);
+ inspector.events.addListener("enable", onGenericEvent);
+ inspector.events.addListener("uninstall", onGenericEvent);
+ inspector.events.addListener("cancel", onGenericEvent);
+
+ AddonManager.getAddonByID(testdata.inspectorid, function(a) {
+ a.userDisabled = true;
+
+ do_check_eq(gLastEvent, "disable");
+
+ // enabling after a disable will only fire a 'cancel' event
+ // see - http://mxr.mozilla.org/seamonkey/source/toolkit/mozapps/extensions/src/nsExtensionManager.js.in#5216
+ a.userDisabled = false;
+ do_check_eq(gLastEvent, "cancel");
+
+ a.uninstall();
+ do_check_eq(gLastEvent, "uninstall");
+
+ a.cancelUninstall();
+ do_check_eq(gLastEvent, "cancel");
+
+ // PREF TESTING
+ // Reset the install event preference, so that we can test it again later
+ // inspector.prefs.get("install-event-fired").reset();
+
+ // test the value of the preference root
+ do_check_eq(extensions.all[0].prefs.root, "extensions.addon1@tests.mozilla.org.");
+
+ // test getting nonexistent values
+ var itemValue = inspector.prefs.getValue(testdata.missing, "default");
+ do_check_eq(itemValue, "default");
+
+ do_check_eq(inspector.prefs.get(testdata.missing), null);
+
+ // test setting and getting a value
+ inspector.prefs.setValue(testdata.dummy, "dummy");
+ itemValue = inspector.prefs.getValue(testdata.dummy, "default");
+ do_check_eq(itemValue, "dummy");
+
+ // test for overwriting an existing value
+ inspector.prefs.setValue(testdata.dummy, "smarty");
+ itemValue = inspector.prefs.getValue(testdata.dummy, "default");
+ do_check_eq(itemValue, "smarty");
+
+ // test setting and getting a value
+ inspector.prefs.get(testdata.dummy).value = "dummy2";
+ itemValue = inspector.prefs.get(testdata.dummy).value;
+ do_check_eq(itemValue, "dummy2");
+
+ // test resetting a pref [since there is no default value, the pref should disappear]
+ inspector.prefs.get(testdata.dummy).reset();
+ itemValue = inspector.prefs.getValue(testdata.dummy, "default");
+ do_check_eq(itemValue, "default");
+
+ // test to see if a non-existant property exists
+ do_check_true(!inspector.prefs.has(testdata.dummy));
+
+ inspector.prefs.events.addListener("change", onPrefChange);
+ inspector.prefs.setValue("fuel.fuel-test", "change event");
+ });
+ });
+}
+
+var gLastEvent;
+function onGenericEvent(event) {
+ gLastEvent = event.type;
+}
+
+function onPrefChange(evt) {
+ Application.getExtensions(function(extensions) {
+ var inspector3 = extensions.get(testdata.inspectorid);
+
+ do_check_eq(evt.data, testdata.dummy);
+ inspector3.prefs.events.removeListener("change", onPrefChange);
+
+ inspector3.prefs.get("fuel.fuel-test").events.addListener("change", onPrefChange2);
+ inspector3.prefs.setValue("fuel.fuel-test", "change event2");
+ });
+}
+
+function onPrefChange2(evt) {
+ do_check_eq(evt.data, testdata.dummy);
+
+ do_execute_soon(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_general.js b/toolkit/mozapps/extensions/test/xpcshell/test_general.js
new file mode 100644
index 000000000..e69b13314
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_general.js
@@ -0,0 +1,58 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This just verifies that the EM can actually startup and shutdown a few times
+// without any errors
+
+// We have to look up how many add-ons are present since there will be plugins
+// etc. detected
+var gCount;
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ var count = 0;
+ startupManager();
+ AddonManager.getAddonsByTypes(null, function(list) {
+ gCount = list.length;
+
+ do_execute_soon(run_test_1);
+ });
+}
+
+function run_test_1() {
+ restartManager();
+
+ AddonManager.getAddonsByTypes(null, function(addons) {
+ do_check_eq(gCount, addons.length);
+
+ AddonManager.getAddonsWithOperationsByTypes(null, function(pendingAddons) {
+ do_check_eq(0, pendingAddons.length);
+
+ do_execute_soon(run_test_2);
+ });
+ });
+}
+
+function run_test_2() {
+ shutdownManager();
+
+ startupManager(false);
+
+ AddonManager.getAddonsByTypes(null, function(addons) {
+ do_check_eq(gCount, addons.length);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+function run_test_3() {
+ restartManager();
+
+ AddonManager.getAddonsByTypes(null, callback_soon(function(addons) {
+ do_check_eq(gCount, addons.length);
+ do_test_finished();
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js b/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js
new file mode 100644
index 000000000..c83638d54
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_getresource.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// install.rdf size, icon.png size, subfile.txt size
+const ADDON_SIZE = 672 + 15 + 26;
+
+// This verifies the functionality of getResourceURI
+// There are two cases - with a filename it returns an nsIFileURL to the filename
+// and with no parameters, it returns an nsIFileURL to the root of the addon
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ startupManager();
+
+ AddonManager.getInstallForFile(do_get_addon("test_getresource"), function(aInstall) {
+ do_check_true(aInstall.addon.hasResource("install.rdf"));
+ do_check_eq(aInstall.addon.getResourceURI().spec, aInstall.sourceURI.spec);
+
+ do_check_true(aInstall.addon.hasResource("icon.png"));
+ do_check_eq(aInstall.addon.getResourceURI("icon.png").spec,
+ "jar:" + aInstall.sourceURI.spec + "!/icon.png");
+
+ do_check_false(aInstall.addon.hasResource("missing.txt"));
+
+ do_check_true(aInstall.addon.hasResource("subdir/subfile.txt"));
+ do_check_eq(aInstall.addon.getResourceURI("subdir/subfile.txt").spec,
+ "jar:" + aInstall.sourceURI.spec + "!/subdir/subfile.txt");
+
+ do_check_false(aInstall.addon.hasResource("subdir/missing.txt"));
+
+ do_check_eq(aInstall.addon.size, ADDON_SIZE);
+
+ completeAllInstalls([aInstall], function() {
+ restartManager();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+
+ let addonDir = gProfD.clone();
+ addonDir.append("extensions");
+ let rootUri = do_get_addon_root_uri(addonDir, "addon1@tests.mozilla.org");
+
+ let uri = a1.getResourceURI("/");
+ do_check_eq(uri.spec, rootUri);
+
+ let file = rootUri + "install.rdf";
+ do_check_true(a1.hasResource("install.rdf"));
+ uri = a1.getResourceURI("install.rdf")
+ do_check_eq(uri.spec, file);
+
+ file = rootUri + "icon.png";
+ do_check_true(a1.hasResource("icon.png"));
+ uri = a1.getResourceURI("icon.png")
+ do_check_eq(uri.spec, file);
+
+ do_check_false(a1.hasResource("missing.txt"));
+
+ file = rootUri + "subdir/subfile.txt";
+ do_check_true(a1.hasResource("subdir/subfile.txt"));
+ uri = a1.getResourceURI("subdir/subfile.txt")
+ do_check_eq(uri.spec, file);
+
+ do_check_false(a1.hasResource("subdir/missing.txt"));
+
+ do_check_eq(a1.size, ADDON_SIZE);
+
+ a1.uninstall();
+
+ try {
+ // hasResource should never throw an exception.
+ do_check_false(a1.hasResource("icon.png"));
+ } catch (e) {
+ do_check_true(false);
+ }
+
+ AddonManager.getInstallForFile(do_get_addon("test_getresource"),
+ callback_soon(function(aInstall_2) {
+ do_check_false(a1.hasResource("icon.png"));
+ do_check_true(aInstall_2.addon.hasResource("icon.png"));
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_eq(newa1, null);
+
+ do_execute_soon(do_test_finished);
+ });
+ }));
+ });
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Device.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Device.js
new file mode 100644
index 000000000..9b0eb54a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Device.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which differs only on device ID, but otherwise
+// exactly matches the blacklist entry, is not blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x9876");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x9876");
+ break;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x9876");
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ gfxInfo.spoofVendorID("abcd");
+ gfxInfo.spoofDeviceID("aabb");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_CANVAS2D_ACCELERATION);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_DriverNew.js
new file mode 100644
index 000000000..f8b783ff2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_DriverNew.js
@@ -0,0 +1,92 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a new-enough driver bypasses the blacklist, even if the rest of
+// the attributes match the blacklist entry.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2202");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on Darwin.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("abcd");
+ gfxInfo.spoofDeviceID("wxyz");
+ gfxInfo.spoofDriverVersion("6");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverNew.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverNew.js
new file mode 100644
index 000000000..1b3410e87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverNew.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which is newer than the equal
+// blacklist entry is allowed.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xdcdc");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.1112");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on OS X.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("dcdc");
+ gfxInfo.spoofDeviceID("uiop");
+ gfxInfo.spoofDriverVersion("6");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "15.0", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_MSAA);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_ANGLE);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_CANVAS2D_ACCELERATION);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverOld.js
new file mode 100644
index 000000000..248c9e7f6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_DriverOld.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which is older than the equal
+// blacklist entry is correctly allowed.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xdcdc");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.1110");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on Darwin.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("dcdc");
+ gfxInfo.spoofDeviceID("uiop");
+ gfxInfo.spoofDriverVersion("4");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_OK.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_OK.js
new file mode 100644
index 000000000..8b8928bb8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Equal_OK.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which exactly matches the equal
+// blacklist entry is successfully blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xdcdc");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.1111");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on Darwin.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("dcdc");
+ gfxInfo.spoofDeviceID("uiop");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_DriverOld.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_DriverOld.js
new file mode 100644
index 000000000..bd5858023
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_DriverOld.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which is lower than the greater-than-or-equal
+// blacklist entry is allowed.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabab");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on Darwin.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("abab");
+ gfxInfo.spoofDeviceID("ghjk");
+ gfxInfo.spoofDriverVersion("6");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_OK.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_OK.js
new file mode 100644
index 000000000..b5c5ed2a6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_GTE_OK.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which exactly matches the greater-than-or-equal
+// blacklist entry is successfully blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabab");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2202");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't support driver versions on Linux.
+ do_test_finished();
+ return;
+ case "Darwin":
+ // We don't support driver versions on Darwin.
+ do_test_finished();
+ return;
+ case "Android":
+ gfxInfo.spoofVendorID("abab");
+ gfxInfo.spoofDeviceID("ghjk");
+ gfxInfo.spoofDriverVersion("7");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_No_Comparison.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_No_Comparison.js
new file mode 100644
index 000000000..ff37e6676
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_No_Comparison.js
@@ -0,0 +1,89 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which exactly matches the blacklist entry is
+// successfully blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x6666");
+
+ // Spoof the OS version so it matches the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ break;
+ case "Darwin":
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var driverVersion = gfxInfo.adapterDriverVersion;
+ if (driverVersion) {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DEVICE);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ }
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OK.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OK.js
new file mode 100644
index 000000000..72b2a2bdc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OK.js
@@ -0,0 +1,94 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which exactly matches the blacklist entry is
+// successfully blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ break;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ gfxInfo.spoofVendorID("abcd");
+ gfxInfo.spoofDeviceID("asdf");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OS.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OS.js
new file mode 100644
index 000000000..fa0deb19a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OS.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which differs only on OS version, but otherwise
+// exactly matches the blacklist entry, is not blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows Vista
+ gfxInfo.spoofOSVersion(0x60000);
+ break;
+ case "Linux":
+ // We don't have any OS versions on Linux, just "Linux".
+ do_test_finished();
+ return;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofOSVersion(0x1080);
+ break;
+ case "Android":
+ // On Android, the driver version is used as the OS version (because
+ // there's so many of them).
+ do_test_finished();
+ return;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_match.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_match.js
new file mode 100644
index 000000000..f01329b45
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_match.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether new OS versions are matched properly.
+// Uses test_gfxBlacklist_OS.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist_OSVersion.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+
+ // Spoof the version of the OS appropriately to test the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ // Windows 8
+ gfxInfo.spoofOSVersion(0x60002);
+ break;
+ case "Linux":
+ // We don't have any OS versions on Linux, just "Linux".
+ do_test_finished();
+ return;
+ case "Darwin":
+ // Mountain Lion
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ // On Android, the driver version is used as the OS version (because
+ // there's so many of them).
+ do_test_finished();
+ return;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ if (get_platform() == "WINNT") {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ } else if (get_platform() == "Darwin") {
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ }
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist_OSVersion.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js
new file mode 100644
index 000000000..4b1069dc6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether blocklists specifying new OSeswcorrectly don't block if driver
+// versions are appropriately up-to-date.
+// Uses test_gfxBlacklist_OS.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist_OSVersion.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ gfxInfo.spoofDriverVersion("8.52.322.2202");
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+
+ // Spoof the version of the OS appropriately to test the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ // Windows 8
+ gfxInfo.spoofOSVersion(0x60002);
+ break;
+ case "Linux":
+ // We don't have any OS versions on Linux, just "Linux".
+ do_test_finished();
+ return;
+ case "Darwin":
+ gfxInfo.spoofOSVersion(0x1080);
+ break;
+ case "Android":
+ // On Android, the driver version is used as the OS version (because
+ // there's so many of them).
+ do_test_finished();
+ return;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ if (get_platform() == "WINNT") {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ } else if (get_platform() == "Darwin") {
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ }
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist_OSVersion.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js
new file mode 100644
index 000000000..0c5a0dcb7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_OSVersion_mismatch_OSVersion.js
@@ -0,0 +1,96 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether old OS versions are not matched when the blacklist contains
+// only new OS versions.
+// Uses test_gfxBlacklist_OS.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist_OSVersion.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+
+ // Spoof the version of the OS appropriately to test the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ // We don't have any OS versions on Linux, just "Linux".
+ do_test_finished();
+ return;
+ case "Darwin":
+ // Lion
+ gfxInfo.spoofOSVersion(0x1080);
+ break;
+ case "Android":
+ // On Android, the driver version is used as the OS version (because
+ // there's so many of them).
+ do_test_finished();
+ return;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ if (get_platform() == "WINNT") {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ } else if (get_platform() == "Darwin") {
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ }
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist_OSVersion.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Vendor.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Vendor.js
new file mode 100644
index 000000000..868c48149
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Vendor.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This should eventually be moved to head_addons.js
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which differs only on vendor, but otherwise
+// exactly matches the blacklist entry, is not blocked.
+// Uses test_gfxBlacklist.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xdcba");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ gfxInfo.spoofVendorID("0xdcba");
+ gfxInfo.spoofDeviceID("0x1234");
+ break;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xdcba");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ gfxInfo.spoofVendorID("dcba");
+ gfxInfo.spoofDeviceID("asdf");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js
new file mode 100755
index 000000000..48174b772
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_Version.js
@@ -0,0 +1,145 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+// Test whether a machine which exactly matches the blacklist entry is
+// successfully blocked.
+// Uses test_gfxBlacklist_AllOS.xml
+
+Cu.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist_AllOS.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Cc["@mozilla.org/xre/app-info;1"]
+ .getService(Ci.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ break;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ gfxInfo.spoofVendorID("abcd");
+ gfxInfo.spoofDeviceID("asdf");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "15.0", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function checkBlacklist()
+ {
+ var failureId = {};
+ var status;
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ do_check_eq(failureId.value, "FEATURE_FAILURE_DL_BLACKLIST_g1");
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ do_check_eq(failureId.value, "FEATURE_FAILURE_DL_BLACKLIST_g2");
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_10_LAYERS, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ do_check_eq(failureId.value, "");
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_10_1_LAYERS, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+ do_check_eq(failureId.value, "");
+
+ // These four pass on Linux independent of the blocklist XML file as the
+ // try machines don't have support.
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_OPENGL_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_OPENGL, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ do_check_eq(failureId.value, "FEATURE_FAILURE_DL_BLACKLIST_g11");
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_ANGLE, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+ do_check_eq(failureId.value, "FEATURE_FAILURE_DL_BLACKLIST_NO_ID");
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBGL_MSAA, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_STAGEFRIGHT, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_HARDWARE_VIDEO_DECODING, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_ANGLE, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DX_INTEROP2, failureId);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(function(aSubject, aTopic, aData) {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(checkBlacklist);
+ }, "blocklist-data-gfxItems", false);
+
+ load_blocklist("test_gfxBlacklist_AllOS.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_prefs.js
new file mode 100644
index 000000000..fbb992879
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gfxBlacklist_prefs.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var { classes: Cc, interfaces: Ci } = Components;
+
+// Test whether the blacklist succesfully adds and removes the prefs that store
+// its decisions when the remote blacklist is changed.
+// Uses test_gfxBlacklist.xml and test_gfxBlacklist2.xml
+
+Components.utils.import("resource://testing-common/httpd.js");
+
+var gTestserver = new HttpServer();
+gTestserver.start(-1);
+gPort = gTestserver.identity.primaryPort;
+mapFile("/data/test_gfxBlacklist.xml", gTestserver);
+mapFile("/data/test_gfxBlacklist2.xml", gTestserver);
+
+function get_platform() {
+ var xulRuntime = Components.classes["@mozilla.org/xre/app-info;1"]
+ .getService(Components.interfaces.nsIXULRuntime);
+ return xulRuntime.OS;
+}
+
+function load_blocklist(file) {
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" +
+ gPort + "/data/" + file);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+}
+
+// Performs the initial setup
+function run_test() {
+ try {
+ var gfxInfo = Cc["@mozilla.org/gfx/info;1"].getService(Ci.nsIGfxInfo);
+ } catch (e) {
+ do_test_finished();
+ return;
+ }
+
+ // We can't do anything if we can't spoof the stuff we need.
+ if (!(gfxInfo instanceof Ci.nsIGfxInfoDebug)) {
+ do_test_finished();
+ return;
+ }
+
+ gfxInfo.QueryInterface(Ci.nsIGfxInfoDebug);
+
+ // Set the vendor/device ID, etc, to match the test file.
+ switch (get_platform()) {
+ case "WINNT":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofDriverVersion("8.52.322.2201");
+ // Windows 7
+ gfxInfo.spoofOSVersion(0x60001);
+ break;
+ case "Linux":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ break;
+ case "Darwin":
+ gfxInfo.spoofVendorID("0xabcd");
+ gfxInfo.spoofDeviceID("0x1234");
+ gfxInfo.spoofOSVersion(0x1090);
+ break;
+ case "Android":
+ gfxInfo.spoofVendorID("abcd");
+ gfxInfo.spoofDeviceID("asdf");
+ gfxInfo.spoofDriverVersion("5");
+ break;
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "3", "8");
+ startupManager();
+
+ do_test_pending();
+
+ function blacklistAdded(aSubject, aTopic, aData)
+ {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(ensureBlacklistSet);
+ }
+ function ensureBlacklistSet()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ do_check_eq(prefs.getIntPref("gfx.blacklist.direct2d"),
+ Ci.nsIGfxInfo.FEATURE_BLOCKED_DRIVER_VERSION);
+
+ Services.obs.removeObserver(blacklistAdded, "blocklist-data-gfxItems");
+ Services.obs.addObserver(blacklistRemoved, "blocklist-data-gfxItems", false);
+ load_blocklist("test_gfxBlacklist2.xml");
+ }
+
+ function blacklistRemoved(aSubject, aTopic, aData)
+ {
+ // If we wait until after we go through the event loop, gfxInfo is sure to
+ // have processed the gfxItems event.
+ do_execute_soon(ensureBlacklistUnset);
+ }
+ function ensureBlacklistUnset()
+ {
+ var status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT2D);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ // Make sure unrelated features aren't affected
+ status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_9_LAYERS);
+ do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ var exists = false;
+ try {
+ prefs.getIntPref("gfx.blacklist.direct2d");
+ exists = true;
+ } catch (e) {}
+
+ do_check_false(exists);
+
+ gTestserver.stop(do_test_finished);
+ }
+
+ Services.obs.addObserver(blacklistAdded, "blocklist-data-gfxItems", false);
+ load_blocklist("test_gfxBlacklist.xml");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js
new file mode 100644
index 000000000..545d7d666
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_gmpProvider.js
@@ -0,0 +1,416 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var {classes: Cc, interfaces: Ci, utils: Cu} = Components;
+var GMPScope = Cu.import("resource://gre/modules/addons/GMPProvider.jsm");
+Cu.import("resource://gre/modules/AppConstants.jsm");
+Cu.import("resource://gre/modules/UpdateUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "pluginsBundle",
+ () => Services.strings.createBundle("chrome://global/locale/plugins.properties"));
+
+XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
+ "resource://gre/modules/FileUtils.jsm");
+
+var gMockAddons = new Map();
+var gMockEmeAddons = new Map();
+
+for (let plugin of GMPScope.GMP_PLUGINS) {
+ let mockAddon = Object.freeze({
+ id: plugin.id,
+ isValid: true,
+ isInstalled: false,
+ nameId: plugin.name,
+ descriptionId: plugin.description,
+ missingKey: plugin.missingKey,
+ missingFilesKey: plugin.missingFilesKey,
+ });
+ gMockAddons.set(mockAddon.id, mockAddon);
+ if (mockAddon.id == "gmp-widevinecdm" ||
+ mockAddon.id.indexOf("gmp-eme-") == 0) {
+ gMockEmeAddons.set(mockAddon.id, mockAddon);
+ }
+}
+
+var gInstalledAddonId = "";
+var gPrefs = Services.prefs;
+var gGetKey = GMPScope.GMPPrefs.getPrefKey;
+
+function MockGMPInstallManager() {
+}
+
+MockGMPInstallManager.prototype = {
+ checkForAddons: () => Promise.resolve({
+ usedFallback: true,
+ gmpAddons: [...gMockAddons.values()]
+ }),
+
+ installAddon: addon => {
+ gInstalledAddonId = addon.id;
+ return Promise.resolve();
+ },
+};
+
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_LOGGING_DUMP, true);
+ gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_LOGGING_LEVEL, 0);
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true);
+ for (let addon of gMockAddons.values()) {
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VISIBLE, addon.id),
+ true);
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_FORCE_SUPPORTED, addon.id),
+ true);
+ }
+ GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+
+ run_next_test();
+}
+
+add_task(function* test_notInstalled() {
+ for (let addon of gMockAddons.values()) {
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id), "");
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false);
+ }
+
+ let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]);
+ Assert.equal(addons.length, gMockAddons.size);
+
+ for (let addon of addons) {
+ Assert.ok(!addon.isInstalled);
+ Assert.equal(addon.type, "plugin");
+ Assert.equal(addon.version, "");
+
+ let mockAddon = gMockAddons.get(addon.id);
+
+ Assert.notEqual(mockAddon, null);
+ let name = pluginsBundle.GetStringFromName(mockAddon.nameId);
+ Assert.equal(addon.name, name);
+ let description = pluginsBundle.GetStringFromName(mockAddon.descriptionId);
+ Assert.equal(addon.description, description);
+
+ Assert.ok(!addon.isActive);
+ Assert.ok(!addon.appDisabled);
+ Assert.ok(addon.userDisabled);
+
+ Assert.equal(addon.blocklistState, Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+ Assert.equal(addon.size, 0);
+ Assert.equal(addon.scope, AddonManager.SCOPE_APPLICATION);
+ Assert.equal(addon.pendingOperations, AddonManager.PENDING_NONE);
+ Assert.equal(addon.operationsRequiringRestart, AddonManager.PENDING_NONE);
+
+ Assert.equal(addon.permissions, AddonManager.PERM_CAN_UPGRADE |
+ AddonManager.PERM_CAN_ENABLE);
+
+ Assert.equal(addon.updateDate, null);
+
+ Assert.ok(addon.isCompatible);
+ Assert.ok(addon.isPlatformCompatible);
+ Assert.ok(addon.providesUpdatesSecurely);
+ Assert.ok(!addon.foreignInstall);
+
+ let mimetypes = addon.pluginMimeTypes;
+ Assert.ok(mimetypes);
+ Assert.equal(mimetypes.length, 0);
+ let libraries = addon.pluginLibraries;
+ Assert.ok(libraries);
+ Assert.equal(libraries.length, 0);
+ Assert.equal(addon.pluginFullpath, "");
+ }
+});
+
+add_task(function* test_installed() {
+ const TEST_DATE = new Date(2013, 0, 1, 12);
+ const TEST_VERSION = "1.2.3.4";
+ const TEST_TIME_SEC = Math.round(TEST_DATE.getTime() / 1000);
+
+ let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]);
+ Assert.equal(addons.length, gMockAddons.size);
+
+ for (let addon of addons) {
+ let mockAddon = gMockAddons.get(addon.id);
+ Assert.notEqual(mockAddon, null);
+
+ let file = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file.append(addon.id);
+ file.append(TEST_VERSION);
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, mockAddon.id), false);
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_LAST_UPDATE, mockAddon.id),
+ "" + TEST_TIME_SEC);
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, mockAddon.id),
+ TEST_VERSION);
+
+ Assert.ok(addon.isInstalled);
+ Assert.equal(addon.type, "plugin");
+ Assert.ok(!addon.isActive);
+ Assert.ok(!addon.appDisabled);
+ Assert.ok(addon.userDisabled);
+
+ let name = pluginsBundle.GetStringFromName(mockAddon.nameId);
+ Assert.equal(addon.name, name);
+ Assert.equal(addon.version, TEST_VERSION);
+
+ Assert.equal(addon.permissions, AddonManager.PERM_CAN_UPGRADE |
+ AddonManager.PERM_CAN_ENABLE);
+
+ Assert.equal(addon.updateDate.getTime(), TEST_TIME_SEC * 1000);
+
+ let mimetypes = addon.pluginMimeTypes;
+ Assert.ok(mimetypes);
+ Assert.equal(mimetypes.length, 0);
+ let libraries = addon.pluginLibraries;
+ Assert.ok(libraries);
+ Assert.equal(libraries.length, 1);
+ Assert.equal(libraries[0], TEST_VERSION);
+ let fullpath = addon.pluginFullpath;
+ Assert.equal(fullpath.length, 1);
+ Assert.equal(fullpath[0], file.path);
+ }
+});
+
+add_task(function* test_enable() {
+ let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]);
+ Assert.equal(addons.length, gMockAddons.size);
+
+ for (let addon of addons) {
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true);
+
+ Assert.ok(addon.isActive);
+ Assert.ok(!addon.appDisabled);
+ Assert.ok(!addon.userDisabled);
+
+ Assert.equal(addon.permissions, AddonManager.PERM_CAN_UPGRADE |
+ AddonManager.PERM_CAN_DISABLE);
+ }
+});
+
+add_task(function* test_globalEmeDisabled() {
+ let addons = yield promiseAddonsByIDs([...gMockEmeAddons.keys()]);
+ Assert.equal(addons.length, gMockEmeAddons.size);
+
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, false);
+ GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+ for (let addon of addons) {
+ Assert.ok(!addon.isActive);
+ Assert.ok(addon.appDisabled);
+ Assert.ok(!addon.userDisabled);
+
+ Assert.equal(addon.permissions, 0);
+ }
+ gPrefs.setBoolPref(GMPScope.GMPPrefs.KEY_EME_ENABLED, true);
+ GMPScope.GMPProvider.shutdown();
+ GMPScope.GMPProvider.startup();
+});
+
+add_task(function* test_autoUpdatePrefPersistance() {
+ let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]);
+ Assert.equal(addons.length, gMockAddons.size);
+
+ for (let addon of addons) {
+ let autoupdateKey = gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id);
+ gPrefs.clearUserPref(autoupdateKey);
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ Assert.ok(!gPrefs.getBoolPref(autoupdateKey));
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ Assert.equal(addon.applyBackgroundUpdates, AddonManager.AUTOUPDATE_ENABLE);
+ Assert.ok(gPrefs.getBoolPref(autoupdateKey));
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+ Assert.ok(!gPrefs.prefHasUserValue(autoupdateKey));
+ }
+});
+
+function createMockPluginFilesIfNeeded(aFile, aPluginId) {
+ function createFile(aFileName) {
+ let f = aFile.clone();
+ f.append(aFileName);
+ if (!f.exists()) {
+ f.create(Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ }
+ }
+
+ let id = aPluginId.substring(4);
+ let libName = AppConstants.DLL_PREFIX + id + AppConstants.DLL_SUFFIX;
+
+ createFile(libName);
+ if (aPluginId == "gmp-widevinecdm") {
+ createFile("manifest.json");
+ } else {
+ createFile(id + ".info");
+ }
+ if (aPluginId == "gmp-eme-adobe")
+ createFile(id + ".voucher");
+}
+
+// Array.includes() is only in Nightly channel, so polyfill so we don't fail
+// on other branches.
+if (![].includes) {
+ Array.prototype.includes = function(element) {
+ return Object(this).indexOf(element) != -1;
+ }
+}
+
+add_task(function* test_pluginRegistration() {
+ const TEST_VERSION = "1.2.3.4";
+
+ let profD = do_get_profile();
+ for (let addon of gMockAddons.values()) {
+ let file = profD.clone();
+ file.append(addon.id);
+ file.append(TEST_VERSION);
+
+ let addedPaths = [];
+ let removedPaths = [];
+ let clearPaths = () => { addedPaths = []; removedPaths = []; }
+
+ let MockGMPService = {
+ addPluginDirectory: path => {
+ if (!addedPaths.includes(path)) {
+ addedPaths.push(path);
+ }
+ },
+ removePluginDirectory: path => {
+ if (!removedPaths.includes(path)) {
+ removedPaths.push(path);
+ }
+ },
+ removeAndDeletePluginDirectory: path => {
+ if (!removedPaths.includes(path)) {
+ removedPaths.push(path);
+ }
+ },
+ };
+ GMPScope.gmpService = MockGMPService;
+
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true);
+
+ // Test that plugin registration fails if the plugin dynamic library and
+ // info files are not present.
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION);
+ clearPaths();
+ yield promiseRestartManager();
+ Assert.equal(addedPaths.indexOf(file.path), -1);
+ Assert.deepEqual(removedPaths, [file.path]);
+
+ // Create dummy GMP library/info files, and test that plugin registration
+ // succeeds during startup, now that we've added GMP info/lib files.
+ createMockPluginFilesIfNeeded(file, addon.id);
+
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION);
+ clearPaths();
+ yield promiseRestartManager();
+ Assert.notEqual(addedPaths.indexOf(file.path), -1);
+ Assert.deepEqual(removedPaths, []);
+
+ // Setting the ABI to something invalid should cause plugin to be removed at startup.
+ clearPaths();
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ABI, addon.id), "invalid-ABI");
+ yield promiseRestartManager();
+ Assert.equal(addedPaths.indexOf(file.path), -1);
+ Assert.deepEqual(removedPaths, [file.path]);
+
+ // Setting the ABI to expected ABI should cause registration at startup.
+ clearPaths();
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION);
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ABI, addon.id), UpdateUtils.ABI);
+ yield promiseRestartManager();
+ Assert.notEqual(addedPaths.indexOf(file.path), -1);
+ Assert.deepEqual(removedPaths, []);
+
+ // Check that clearing the version doesn't trigger registration.
+ clearPaths();
+ gPrefs.clearUserPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id));
+ Assert.deepEqual(addedPaths, []);
+ Assert.deepEqual(removedPaths, [file.path]);
+
+ // Restarting with no version set should not trigger registration.
+ clearPaths();
+ yield promiseRestartManager();
+ Assert.equal(addedPaths.indexOf(file.path), -1);
+ Assert.equal(removedPaths.indexOf(file.path), -1);
+
+ // Changing the pref mid-session should cause unregistration and registration.
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION);
+ clearPaths();
+ const TEST_VERSION_2 = "5.6.7.8";
+ let file2 = Services.dirsvc.get("ProfD", Ci.nsIFile);
+ file2.append(addon.id);
+ file2.append(TEST_VERSION_2);
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION_2);
+ Assert.deepEqual(addedPaths, [file2.path]);
+ Assert.deepEqual(removedPaths, [file.path]);
+
+ // Disabling the plugin should cause unregistration.
+ gPrefs.setCharPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_VERSION, addon.id),
+ TEST_VERSION);
+ clearPaths();
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), false);
+ Assert.deepEqual(addedPaths, []);
+ Assert.deepEqual(removedPaths, [file.path]);
+
+ // Restarting with the plugin disabled should not cause registration.
+ clearPaths();
+ yield promiseRestartManager();
+ Assert.equal(addedPaths.indexOf(file.path), -1);
+ Assert.equal(removedPaths.indexOf(file.path), -1);
+
+ // Re-enabling the plugin should cause registration.
+ clearPaths();
+ gPrefs.setBoolPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_ENABLED, addon.id), true);
+ Assert.deepEqual(addedPaths, [file.path]);
+ Assert.deepEqual(removedPaths, []);
+ GMPScope = Cu.import("resource://gre/modules/addons/GMPProvider.jsm");
+ }
+});
+
+add_task(function* test_periodicUpdate() {
+ Object.defineProperty(GMPScope, "GMPInstallManager", {
+ value: MockGMPInstallManager,
+ writable: true,
+ enumerable: true,
+ configurable: true
+ });
+
+ let addons = yield promiseAddonsByIDs([...gMockAddons.keys()]);
+ Assert.equal(addons.length, gMockAddons.size);
+
+ for (let addon of addons) {
+ gPrefs.clearUserPref(gGetKey(GMPScope.GMPPrefs.KEY_PLUGIN_AUTOUPDATE, addon.id));
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK, 0);
+ let result =
+ yield addon.findUpdates({}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ Assert.strictEqual(result, false);
+
+ addon.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK, Date.now() / 1000 - 60);
+ result =
+ yield addon.findUpdates({}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ Assert.strictEqual(result, false);
+
+ gPrefs.setIntPref(GMPScope.GMPPrefs.KEY_UPDATE_LAST_CHECK,
+ Date.now() / 1000 - 2 * GMPScope.SEC_IN_A_DAY);
+ gInstalledAddonId = "";
+ result =
+ yield addon.findUpdates({}, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ Assert.strictEqual(result, true);
+ Assert.equal(gInstalledAddonId, addon.id);
+ }
+
+ GMPScope = Cu.import("resource://gre/modules/addons/GMPProvider.jsm");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js b/toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js
new file mode 100644
index 000000000..925e63626
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hasbinarycomponents.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests detection of binary components via parsing of chrome manifests.
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ startupManager();
+
+ installAllFiles([do_get_addon("test_chromemanifest_1"),
+ do_get_addon("test_chromemanifest_2"),
+ do_get_addon("test_chromemanifest_3"),
+ do_get_addon("test_chromemanifest_4"),
+ do_get_addon("test_chromemanifest_5")],
+ function() {
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+ // addon1 has no binary components
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.hasBinaryComponents);
+ do_check_true(a1.isCompatible);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ // addon2 has a binary component, is compatible
+ do_check_neq(a2, null);
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.hasBinaryComponents);
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ // addon3 has a binary component, is incompatible
+ do_check_neq(a3, null);
+ do_check_false(a3.userDisabled);
+ do_check_true(a2.hasBinaryComponents);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ // addon4 has a binary component listed in a sub-manifest, is incompatible
+ do_check_neq(a4, null);
+ do_check_false(a4.userDisabled);
+ do_check_true(a2.hasBinaryComponents);
+ do_check_false(a4.isCompatible);
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ // addon5 has a binary component, but is set to not unpack
+ do_check_neq(a5, null);
+ do_check_false(a5.userDisabled);
+ if (TEST_UNPACKED)
+ do_check_true(a5.hasBinaryComponents);
+ else
+ do_check_false(a5.hasBinaryComponents);
+ do_check_true(a5.isCompatible);
+ do_check_false(a5.appDisabled);
+ do_check_true(a5.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_execute_soon(do_test_finished);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
new file mode 100644
index 000000000..c9303897f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix.js
@@ -0,0 +1,309 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that hotfix installation works
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// Ignore any certificate requirements the app has set
+Services.prefs.setBoolPref("extensions.hotfix.cert.checkAttributes", false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+mapFile("/data/test_hotfix_1.rdf", testserver);
+mapFile("/data/test_hotfix_2.rdf", testserver);
+mapFile("/data/test_hotfix_3.rdf", testserver);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+
+ do_test_pending();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+// Test that background updates find and install any available hotfix
+function run_test_1() {
+ Services.prefs.setCharPref("extensions.hotfix.id", "hotfix@tests.mozilla.org");
+ Services.prefs.setCharPref("extensions.update.background.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_1.rdf");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_1));
+
+ // We don't need to wait on the promise, just waiting for the install to finish is enough.
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function check_test_1() {
+ restartManager();
+
+ AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "1.0");
+
+ aAddon.uninstall();
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Don't install an already used hotfix
+function run_test_2() {
+ restartManager();
+
+ AddonManager.addInstallListener({
+ onNewInstall: function() {
+ do_throw("Should not have seen a new install created");
+ }
+ });
+
+ // Run the background update
+ AddonManagerInternal.backgroundUpdateCheck().then(run_test_3);
+}
+
+// Install a newer hotfix
+function run_test_3() {
+ restartManager();
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_2.rdf");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_3));
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function check_test_3() {
+ restartManager();
+
+ AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "2.0");
+
+ aAddon.uninstall();
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Don't install an incompatible hotfix
+function run_test_4() {
+ restartManager();
+
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_3.rdf");
+
+ AddonManager.addInstallListener({
+ onNewInstall: function() {
+ do_throw("Should not have seen a new install created");
+ }
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck().then(run_test_5);
+}
+
+// Don't install an older hotfix
+function run_test_5() {
+ restartManager();
+
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_1.rdf");
+
+ AddonManager.addInstallListener({
+ onNewInstall: function() {
+ do_throw("Should not have seen a new install created");
+ }
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck().then(run_test_6);
+}
+
+// Don't re-download an already pending install
+function run_test_6() {
+ restartManager();
+
+ Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_1.rdf");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_6));
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function check_test_6() {
+ AddonManager.addInstallListener({
+ onNewInstall: function() {
+ do_throw("Should not have seen a new install created");
+ }
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck()
+ .then(promiseRestartManager)
+ .then(() => promiseAddonByID("hotfix@tests.mozilla.org"))
+ .then(aAddon => {
+ aAddon.uninstall();
+ run_test_7();
+ });
+}
+
+// Start downloading again if something cancels the install
+function run_test_7() {
+ restartManager();
+
+ Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_7);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function check_test_7(aInstall) {
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled",
+ ]);
+
+ aInstall.cancel();
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(finish_test_7));
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function finish_test_7() {
+ restartManager();
+
+ AddonManager.getAddonByID("hotfix@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "1.0");
+
+ aAddon.uninstall();
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Cancel a pending install when a newer version is already available
+function run_test_8() {
+ restartManager();
+
+ Services.prefs.setCharPref("extensions.hotfix.lastVersion", "0");
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_1.rdf");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_8);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function check_test_8() {
+ Services.prefs.setCharPref("extensions.hotfix.url", "http://localhost:" +
+ gPort + "/data/test_hotfix_2.rdf");
+
+ prepare_test({
+ "hotfix@tests.mozilla.org": [
+ "onOperationCancelled",
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallCancelled",
+ "onInstallEnded",
+ ], finish_test_8);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+}
+
+function finish_test_8() {
+ AddonManager.getAllInstalls(callback_soon(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0].version, "2.0");
+
+ restartManager();
+
+ AddonManager.getAddonByID("hotfix@tests.mozilla.org", callback_soon(function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.version, "2.0");
+
+ aAddon.uninstall();
+ restartManager();
+
+ end_test();
+ }));
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_hotfix_cert.js b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix_cert.js
new file mode 100644
index 000000000..42ee59740
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_hotfix_cert.js
@@ -0,0 +1,167 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that hotfix installation works
+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.";
+
+// Derived from "openssl x509 -in firefox-hotfix.crt -fingerprint -sha1"
+const GOOD_FINGERPRINT = "39:E7:2B:7A:5B:CF:37:78:F9:5D:4A:E0:53:2D:2F:3D:68:53:C5:60";
+const BAD_FINGERPRINT = "40:E7:2B:7A:5B:CF:37:78:F9:5D:4A:E0:53:2D:2F:3D:68:53:C5:60";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+testserver.registerDirectory("/data/", do_get_file("data"));
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// Ignore any certificate requirements the app has set
+Services.prefs.setBoolPref(PREF_EM_CERT_CHECKATTRIBUTES, true);
+Services.prefs.setCharPref(PREF_EM_HOTFIX_URL, "http://localhost:" + gPort + "/hotfix.rdf");
+// Clear out all hotfix cert prefs to make sure only the test prefs apply.
+var defaults = Services.prefs.getDefaultBranch("");
+defaults.deleteBranch(PREF_EM_HOTFIX_CERTS);
+
+/*
+ * Register an addon install listener and return a promise that:
+ * resolves with the AddonInstall object if the install succeeds
+ * rejects with the AddonInstall if the install fails
+ */
+function promiseInstallListener() {
+ return new Promise((resolve, reject) => {
+ let listener = {
+ onDownloadFailed: ai => {
+ AddonManager.removeInstallListener(listener);
+ reject(ai);
+ },
+ onInstallEnded: ai => {
+ AddonManager.removeInstallListener(listener);
+ resolve(ai);
+ },
+ onDownloadCancelled: ai => {
+ AddonManager.removeInstallListener(listener);
+ reject(ai);
+ }
+ };
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+function promiseSuccessfulInstall() {
+ return promiseInstallListener().then(
+ aInstall => {
+ do_check_true(true);
+ do_check_eq(aInstall.addon.id, Services.prefs.getCharPref(PREF_EM_HOTFIX_ID));
+ aInstall.addon.uninstall();
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+ },
+ aInstall => {
+ do_throw("Install should not have failed");
+ });
+}
+
+function promiseFailedInstall() {
+ return promiseInstallListener().then(
+ aInstall => {
+ do_throw("Install should not have succeeded");
+ aInstall.addon.uninstall();
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_LASTVERSION);
+ },
+ aInstall => {
+ do_check_true(true);
+ });
+}
+
+var tryInstallHotfix = Task.async(function*(id, file, installListener) {
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_ID, id);
+
+ testserver.registerPathHandler("/hotfix.rdf", function(request, response) {
+ response.write(createUpdateRDF({
+ [id]: [{
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "*",
+ updateLink: "http://localhost:" + gPort + "/data/signing_checks/" + file,
+ }]
+ }]
+ }));
+ });
+
+ yield Promise.all([
+ installListener,
+ AddonManagerPrivate.backgroundUpdateCheck()
+ ]);
+
+ testserver.registerPathHandler("/hotfix.rdf", null);
+ Services.prefs.clearUserPref(PREF_EM_HOTFIX_ID);
+});
+
+// Test valid AMO hotfix signed add-ons doesn't work if the fingerprint pref is wrong
+add_task(function* amo_signed_hotfix() {
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", BAD_FINGERPRINT);
+
+ yield tryInstallHotfix("firefox-hotfix@mozilla.org",
+ "hotfix_good.xpi",
+ promiseFailedInstall());
+});
+
+// Test valid AMO hotfix signed add-ons works
+add_task(function* amo_signed_hotfix() {
+ Services.prefs.setCharPref(PREF_EM_HOTFIX_CERTS + "1.sha1Fingerprint", GOOD_FINGERPRINT);
+
+ yield tryInstallHotfix("firefox-hotfix@mozilla.org",
+ "hotfix_good.xpi",
+ promiseSuccessfulInstall());
+});
+
+// A hotfix altered after signing should fail
+add_task(function* amo_broken_hotfix() {
+ yield tryInstallHotfix("firefox-hotfix@mozilla.org",
+ "hotfix_broken.xpi",
+ promiseFailedInstall());
+});
+
+// Test an add-on with the wrong ID but signed by the right cert fails
+add_task(function* amo_wrongID_rightcert() {
+ yield tryInstallHotfix("test@tests.mozilla.org",
+ "hotfix_badid.xpi",
+ promiseFailedInstall());
+});
+
+// It shouldn't matter that it requested the ID matching the cert to begin with
+// if the embedded cert's ID doesn't match the add-on's ID
+add_task(function* amo_wrongID_rightcert2() {
+ yield tryInstallHotfix("firefox-hotfix@mozilla.org",
+ "hotfix_badid.xpi",
+ promiseFailedInstall());
+});
+
+// Test something signed by a regular AMO cert doesn't work
+add_task(function* amo_signed_addon() {
+ yield tryInstallHotfix("test@tests.mozilla.org",
+ "signed_bootstrap_1.xpi",
+ promiseFailedInstall());
+});
+
+// Test totally unsigned add-on fails
+add_task(function* unsigned() {
+ yield tryInstallHotfix("test@tests.mozilla.org",
+ "unsigned_bootstrap_2.xpi",
+ promiseFailedInstall());
+});
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ startupManager();
+
+ run_next_test();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
new file mode 100644
index 000000000..60af3a9fd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install.js
@@ -0,0 +1,1843 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons can be installed from XPI files
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+// install.rdf size, icon.png, icon64.png size
+const ADDON1_SIZE = 705 + 16 + 16;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+var testserver;
+var gInstallDate;
+var gInstall = null;
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ // Make sure we only register once despite multiple calls
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerPathHandler("/redirect", function(aRequest, aResponse) {
+ aResponse.setStatusLine(null, 301, "Moved Permanently");
+ let url = aRequest.host + ":" + aRequest.port + aRequest.queryString;
+ aResponse.setHeader("Location", "http://" + url);
+ });
+ testserver.start(-1);
+ gPort = testserver.identity.primaryPort;
+
+ do_test_pending();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+// Checks that an install from a local file proceeds as expected
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install1"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_neq(install.addon.syncGUID, null);
+ do_check_eq(install.addon.install, install);
+ do_check_eq(install.addon.size, ADDON1_SIZE);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+ let file = do_get_addon("test_install1");
+ let uri = Services.io.newFileURI(file).spec;
+ do_check_eq(install.addon.getResourceURI("install.rdf").spec, "jar:" + uri + "!/install.rdf");
+ do_check_eq(install.addon.iconURL, "jar:" + uri + "!/icon.png");
+ do_check_eq(install.addon.icon64URL, "jar:" + uri + "!/icon64.png");
+ do_check_eq(install.iconURL, null);
+
+ do_check_eq(install.sourceURI.spec, uri);
+ do_check_eq(install.addon.sourceURI.spec, uri);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ AddonManager.getInstallsByTypes(["foo"], function(fooInstalls) {
+ do_check_eq(fooInstalls.length, 0);
+
+ AddonManager.getInstallsByTypes(["extension"], function(extensionInstalls) {
+ do_check_eq(extensionInstalls.length, 1);
+ do_check_eq(extensionInstalls[0], install);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ check_test_1(install.addon.syncGUID);
+ });
+ install.install();
+ });
+ });
+ });
+ });
+}
+
+function check_test_1(installSyncGUID) {
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(olda1) {
+ do_check_eq(olda1, null);
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(pendingAddons) {
+ do_check_eq(pendingAddons.length, 1);
+ do_check_eq(pendingAddons[0].id, "addon1@tests.mozilla.org");
+ let uri = NetUtil.newURI(pendingAddons[0].iconURL);
+ if (uri instanceof AM_Ci.nsIJARURI) {
+ let jarURI = uri.QueryInterface(AM_Ci.nsIJARURI);
+ let archiveURI = jarURI.JARFile;
+ let archiveFile = archiveURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(archiveFile);
+ do_check_true(zipReader.hasEntry(jarURI.JAREntry));
+ }
+ finally {
+ zipReader.close();
+ }
+ }
+ else {
+ let iconFile = uri.QueryInterface(AM_Ci.nsIFileURL).file;
+ do_check_true(iconFile.exists());
+ // Make the iconFile predictably old.
+ iconFile.lastModifiedTime = Date.now() - MAKE_FILE_OLD_DIFFERENCE;
+ }
+
+ // Make the pending install have a sensible date
+ let updateDate = Date.now();
+ let extURI = pendingAddons[0].getResourceURI("");
+ let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ setExtensionModifiedTime(ext, updateDate);
+
+ // The pending add-on cannot be disabled or enabled.
+ do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_DISABLE));
+
+ restartManager();
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls, 0);
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_neq(a1, null);
+ do_check_neq(a1.syncGUID, null);
+ do_check_true(a1.syncGUID.length >= 9);
+ do_check_eq(a1.syncGUID, installSyncGUID);
+ do_check_eq(a1.type, "extension");
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.name, "Test 1");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(do_get_addon("test_install1").exists());
+ do_check_in_crash_annotation(a1.id, a1.version);
+ do_check_eq(a1.size, ADDON1_SIZE);
+ do_check_false(a1.foreignInstall);
+
+ do_check_eq(a1.sourceURI.spec,
+ Services.io.newFileURI(do_get_addon("test_install1")).spec);
+ let difference = a1.installDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on install time was out by " + difference + "ms");
+
+ difference = a1.updateDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on update time was out by " + difference + "ms");
+
+ do_check_true(a1.hasResource("install.rdf"));
+ do_check_false(a1.hasResource("foo.bar"));
+
+ let uri2 = do_get_addon_root_uri(profileDir, "addon1@tests.mozilla.org");
+ do_check_eq(a1.getResourceURI("install.rdf").spec, uri2 + "install.rdf");
+ do_check_eq(a1.iconURL, uri2 + "icon.png");
+ do_check_eq(a1.icon64URL, uri2 + "icon64.png");
+
+ // Ensure that extension bundle (or icon if unpacked) has updated
+ // lastModifiedDate.
+ let testURI = a1.getResourceURI(TEST_UNPACKED ? "icon.png" : "");
+ let testFile = testURI.QueryInterface(Components.interfaces.nsIFileURL).file;
+ do_check_true(testFile.exists());
+ difference = testFile.lastModifiedTime - Date.now();
+ do_check_true(Math.abs(difference) < MAX_TIME_DIFFERENCE);
+
+ a1.uninstall();
+ let { id, version } = a1;
+ restartManager();
+ do_check_not_in_crash_annotation(id, version);
+
+ do_execute_soon(run_test_2);
+ }));
+ });
+ }));
+ });
+}
+
+// Tests that an install from a url downloads.
+function run_test_2() {
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test 2");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.iconURL, null);
+ do_check_eq(install.sourceURI.spec, url);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_2);
+
+ install.addListener({
+ onDownloadProgress: function() {
+ do_execute_soon(function() {
+ Components.utils.forceGC();
+ });
+ }
+ });
+
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 2", null, "1.0");
+}
+
+function check_test_2(install) {
+ ensure_test_completed();
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.name, "Real Test 2");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.addon.install, install);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+ do_check_eq(install.iconURL, null);
+
+ // Pause the install here and start it again in run_test_3
+ do_execute_soon(function() { run_test_3(install); });
+ return false;
+}
+
+// Tests that the downloaded XPI installs ok
+function run_test_3(install) {
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_3);
+ install.install();
+}
+
+function check_test_3(aInstall) {
+ // Make the pending install have a sensible date
+ let updateDate = Date.now();
+ let extURI = aInstall.addon.getResourceURI("");
+ let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ setExtensionModifiedTime(ext, updateDate);
+
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+ do_check_eq(olda2, null);
+ restartManager();
+
+ AddonManager.getAllInstalls(function(installs) {
+ do_check_eq(installs, 0);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_neq(a2.syncGUID, null);
+ do_check_eq(a2.type, "extension");
+ do_check_eq(a2.version, "2.0");
+ do_check_eq(a2.name, "Real Test 2");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(do_get_addon("test_install2_1").exists());
+ do_check_in_crash_annotation(a2.id, a2.version);
+ do_check_eq(a2.sourceURI.spec,
+ "http://localhost:" + gPort + "/addons/test_install2_1.xpi");
+
+ let difference = a2.installDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on install time was out by " + difference + "ms");
+
+ difference = a2.updateDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on update time was out by " + difference + "ms");
+
+ gInstallDate = a2.installDate.getTime();
+
+ run_test_4();
+ });
+ });
+ }));
+}
+
+// Tests that installing a new version of an existing add-on works
+function run_test_4() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Test 3");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+ do_check_eq(install.existingAddon, null);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_4);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 3", null, "3.0");
+}
+
+function check_test_4(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Real Test 3");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_neq(install.existingAddon);
+ do_check_eq(install.existingAddon.id, "addon2@tests.mozilla.org");
+ do_check_eq(install.addon.install, install);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ run_test_5();
+ // Installation will continue when there is nothing returned.
+}
+
+// Continue installing the new version
+function run_test_5() {
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_5);
+}
+
+function check_test_5(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(olda2) {
+ do_check_neq(olda2, null);
+ do_check_true(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ AddonManager.getInstallsByTypes(null, callback_soon(function(installs) {
+ do_check_eq(installs.length, 1);
+ do_check_eq(installs[0].addon, olda2.pendingUpgrade);
+ restartManager();
+
+ AddonManager.getInstallsByTypes(null, function(installs2) {
+ do_check_eq(installs2.length, 0);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_eq(a2.type, "extension");
+ do_check_eq(a2.version, "3.0");
+ do_check_eq(a2.name, "Real Test 3");
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(do_get_addon("test_install2_2").exists());
+ do_check_in_crash_annotation(a2.id, a2.version);
+ do_check_eq(a2.sourceURI.spec,
+ "http://localhost:" + gPort + "/addons/test_install2_2.xpi");
+ do_check_false(a2.foreignInstall);
+
+ do_check_eq(a2.installDate.getTime(), gInstallDate);
+ // Update date should be later (or the same if this test is too fast)
+ do_check_true(a2.installDate <= a2.updateDate);
+
+ a2.uninstall();
+ do_execute_soon(run_test_6);
+ });
+ });
+ }));
+ });
+}
+
+// Tests that an install that requires a compatibility update works
+function run_test_6() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_6);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_6(install) {
+ ensure_test_completed();
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.existingAddon, null);
+ do_check_false(install.addon.appDisabled);
+ run_test_7();
+ return true;
+}
+
+// Continue the install
+function run_test_7() {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_7);
+}
+
+function check_test_7() {
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(function(olda3) {
+ do_check_eq(olda3, null);
+ restartManager();
+
+ AddonManager.getAllInstalls(function(installs) {
+ do_check_eq(installs, 0);
+
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_neq(a3.syncGUID, null);
+ do_check_eq(a3.type, "extension");
+ do_check_eq(a3.version, "1.0");
+ do_check_eq(a3.name, "Real Test 4");
+ do_check_true(a3.isActive);
+ do_check_false(a3.appDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(do_get_addon("test_install3").exists());
+ a3.uninstall();
+ do_execute_soon(run_test_8);
+ });
+ });
+ }));
+}
+
+function run_test_8() {
+ restartManager();
+
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install3"), function(install) {
+ do_check_true(install.addon.isCompatible);
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_8));
+ install.install();
+ });
+}
+
+function check_test_8() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_neq(a3.syncGUID, null);
+ do_check_eq(a3.type, "extension");
+ do_check_eq(a3.version, "1.0");
+ do_check_eq(a3.name, "Real Test 4");
+ do_check_true(a3.isActive);
+ do_check_false(a3.appDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(do_get_addon("test_install3").exists());
+ a3.uninstall();
+ do_execute_soon(run_test_9);
+ });
+}
+
+// Test that after cancelling a download it is removed from the active installs
+function run_test_9() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_9);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_9(install) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ], function() {
+ let file = install.file;
+
+ // Allow the file removal to complete
+ do_execute_soon(function() {
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 0);
+ do_check_false(file.exists());
+
+ run_test_10();
+ });
+ });
+ });
+
+ install.cancel();
+}
+
+// Tests that after cancelling a pending install it is removed from the active
+// installs
+function run_test_10() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], check_test_10);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_10(install) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ install.cancel();
+
+ ensure_test_completed();
+
+ AddonManager.getAllInstalls(callback_soon(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 0);
+
+ restartManager();
+
+ // Check that the install did not complete
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_eq(a3, null);
+
+ do_execute_soon(run_test_11);
+ });
+ }));
+}
+
+// Tests that a multi-package install shows up as multiple installs with the
+// correct sourceURI.
+function run_test_11() {
+ prepare_test({ }, [
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) {
+ ensure_test_completed();
+ do_check_neq(install, null);
+ do_check_neq(install.linkedInstalls, null);
+ do_check_eq(install.linkedInstalls.length, 5);
+
+ // Might be in any order so sort them based on ID
+ let installs = [install].concat(install.linkedInstalls);
+ installs.sort(function(a, b) {
+ if (a.state != b.state) {
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 1;
+ else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return -1;
+ }
+
+ // Don't care what order the failed installs show up in
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 0;
+
+ if (a.addon.id < b.addon.id)
+ return -1;
+ if (a.addon.id > b.addon.id)
+ return 1;
+ return 0;
+ });
+
+ // Comes from addon4.xpi and is made compatible by an update check
+ do_check_eq(installs[0].sourceURI, install.sourceURI);
+ do_check_eq(installs[0].addon.id, "addon4@tests.mozilla.org");
+ do_check_false(installs[0].addon.appDisabled);
+ do_check_eq(installs[0].version, "1.0");
+ do_check_eq(installs[0].name, "Multi Test 1");
+ do_check_eq(installs[0].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[0].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon5.jar and is compatible by default
+ do_check_eq(installs[1].sourceURI, install.sourceURI);
+ do_check_eq(installs[1].addon.id, "addon5@tests.mozilla.org");
+ do_check_false(installs[1].addon.appDisabled);
+ do_check_eq(installs[1].version, "3.0");
+ do_check_eq(installs[1].name, "Multi Test 2");
+ do_check_eq(installs[1].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[1].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon6.xpi and would be incompatible with strict compat enabled
+ do_check_eq(installs[2].sourceURI, install.sourceURI);
+ do_check_eq(installs[2].addon.id, "addon6@tests.mozilla.org");
+ do_check_false(installs[2].addon.appDisabled);
+ do_check_eq(installs[2].version, "2.0");
+ do_check_eq(installs[2].name, "Multi Test 3");
+ do_check_eq(installs[2].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[2].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon7.jar and is made compatible by an update check
+ do_check_eq(installs[3].sourceURI, install.sourceURI);
+ do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
+ do_check_false(installs[3].addon.appDisabled);
+ do_check_eq(installs[3].version, "5.0");
+ do_check_eq(installs[3].name, "Multi Test 4");
+ do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 4);
+
+ prepare_test({
+ "addon4@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, {
+ "addon4@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ]
+ }, callback_soon(check_test_11));
+
+ installs[0].install();
+ installs[1].install();
+ installs[3].install();
+
+ // Note that we install addon6 last. Since it doesn't need a restart to
+ // install it completes asynchronously which would otherwise make the
+ // onInstallStarted/onInstallEnded events go out of sequence unless this
+ // is the last install operation
+ installs[2].install();
+ });
+ });
+}
+
+function check_test_11() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a4, a5, a6, a7]) {
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+ do_check_neq(a6, null);
+ do_check_neq(a7, null);
+
+ a4.uninstall();
+ a5.uninstall();
+ a6.uninstall();
+ a7.uninstall();
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+// Same as test 11 but for a remote XPI
+function run_test_12() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall",
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install4.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ gInstall = install;
+
+ ensure_test_completed();
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({
+ "addon4@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, {
+ "NO_ID": [
+ "onDownloadStarted",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onDownloadEnded"
+ ],
+ "addon4@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ]
+ }, callback_soon(check_test_12));
+ install.install();
+ }, "application/x-xpinstall", null, "Multi Test 4");
+}
+
+function check_test_12() {
+ do_check_eq(gInstall.linkedInstalls.length, 5);
+
+ // Might be in any order so sort them based on ID
+ let installs = [gInstall].concat(gInstall.linkedInstalls);
+ installs.sort(function(a, b) {
+ if (a.state != b.state) {
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 1;
+ else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return -1;
+ }
+
+ // Don't care what order the failed installs show up in
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 0;
+
+ if (a.addon.id < b.addon.id)
+ return -1;
+ if (a.addon.id > b.addon.id)
+ return 1;
+ return 0;
+ });
+
+ // Comes from addon4.xpi and is made compatible by an update check
+ do_check_eq(installs[0].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[0].addon.id, "addon4@tests.mozilla.org");
+ do_check_false(installs[0].addon.appDisabled);
+ do_check_eq(installs[0].version, "1.0");
+ do_check_eq(installs[0].name, "Multi Test 1");
+ do_check_eq(installs[0].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon5.jar and is compatible by default
+ do_check_eq(installs[1].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[1].addon.id, "addon5@tests.mozilla.org");
+ do_check_false(installs[1].addon.appDisabled);
+ do_check_eq(installs[1].version, "3.0");
+ do_check_eq(installs[1].name, "Multi Test 2");
+ do_check_eq(installs[1].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon6.xpi and would be incompatible with strict compat enabled
+ do_check_eq(installs[2].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[2].addon.id, "addon6@tests.mozilla.org");
+ do_check_false(installs[2].addon.appDisabled);
+ do_check_eq(installs[2].version, "2.0");
+ do_check_eq(installs[2].name, "Multi Test 3");
+ do_check_eq(installs[2].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon7.jar and is made compatible by an update check
+ do_check_eq(installs[3].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
+ do_check_false(installs[3].addon.appDisabled);
+ do_check_eq(installs[3].version, "5.0");
+ do_check_eq(installs[3].name, "Multi Test 4");
+ do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED);
+
+ do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a4, a5, a6, a7]) {
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+ do_check_neq(a6, null);
+ do_check_neq(a7, null);
+
+ a4.uninstall();
+ a5.uninstall();
+ a6.uninstall();
+ a7.uninstall();
+
+ do_execute_soon(run_test_13);
+ });
+}
+
+
+// Tests that cancelling an upgrade leaves the original add-on's pendingOperations
+// correct
+function run_test_13() {
+ restartManager();
+
+ installAllFiles([do_get_addon("test_install2_1")], function() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Test 3");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+ do_check_eq(install.existingAddon, null);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_13);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 3", null, "3.0");
+ });
+}
+
+function check_test_13(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Real Test 3");
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_neq(install.existingAddon, null);
+ do_check_eq(install.existingAddon.id, "addon2@tests.mozilla.org");
+ do_check_eq(install.addon.install, install);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+ do_check_neq(olda2, null);
+ do_check_true(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+ do_check_eq(olda2.pendingUpgrade, install.addon);
+
+ do_check_true(hasFlag(install.addon.pendingOperations,
+ AddonManager.PENDING_INSTALL));
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled",
+ ]);
+
+ install.cancel();
+
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ do_check_false(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+ do_check_eq(olda2.pendingUpgrade, null);
+
+ restartManager();
+
+ // Check that the upgrade did not complete
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.version, "2.0");
+
+ a2.uninstall();
+
+ do_execute_soon(run_test_14);
+ });
+ }));
+}
+
+// Check that cancelling the install from onDownloadStarted actually cancels it
+function run_test_14() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.file, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted"
+ ], check_test_14);
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_14(install) {
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ], function() {
+ let file = install.file;
+
+ install.addListener({
+ onDownloadProgress: function() {
+ do_throw("Download should not have continued");
+ },
+ onDownloadEnded: function() {
+ do_throw("Download should not have continued");
+ }
+ });
+
+ // Allow the listener to return to see if it continues downloading. The
+ // The listener only really tests if we give it time to see progress, the
+ // file check isn't ideal either
+ do_execute_soon(function() {
+ do_check_false(file.exists());
+
+ run_test_15();
+ });
+ });
+
+ // Wait for the channel to be ready to cancel
+ do_execute_soon(function() {
+ install.cancel();
+ });
+}
+
+// Checks that cancelling the install from onDownloadEnded actually cancels it
+function run_test_15() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.file, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], check_test_15);
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_15(install) {
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ]);
+
+ install.cancel();
+
+ ensure_test_completed();
+
+ install.addListener({
+ onInstallStarted: function() {
+ do_throw("Install should not have continued");
+ }
+ });
+
+ // Allow the listener to return to see if it starts installing
+ do_execute_soon(run_test_16);
+}
+
+// Verify that the userDisabled value carries over to the upgrade by default
+function run_test_16() {
+ restartManager();
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall.addon.userDisabled);
+ aInstall.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function install2_1_ended() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+
+ let url_2 = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function install2_2_ended() {
+ do_check_true(aInstall_2.addon.userDisabled);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_17);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that changing the userDisabled value before onInstallEnded works
+function run_test_17() {
+ restartManager();
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function install2_1_ended2() {
+ do_check_false(aInstall.addon.userDisabled);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.isActive);
+
+ let url_2 = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall_2.addon.userDisabled);
+ aInstall_2.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function install2_2_ended2() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_18);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that changing the userDisabled value before onInstallEnded works
+function run_test_18() {
+ restartManager();
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall.addon.userDisabled);
+ aInstall.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function install_2_1_ended3() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+
+ let url_2 = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallStarted: function() {
+ do_check_true(aInstall_2.addon.userDisabled);
+ aInstall_2.addon.userDisabled = false;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function install_2_2_ended3() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_false(a2_2.userDisabled);
+ do_check_true(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_18_1);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+
+// Checks that metadata is not stored if the pref is set to false
+function run_test_18_1() {
+ restartManager();
+
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS,
+ "http://localhost:" + gPort + "/data/test_install.xml");
+
+ Services.prefs.setBoolPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", false);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test18_1_install_ended() {
+ do_check_neq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_19);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Checks that metadata is downloaded for new installs and is visible before and
+// after restart
+function run_test_19() {
+ restartManager();
+ Services.prefs.setBoolPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", true);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test19_install_ended() {
+ do_check_eq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_20);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Do the same again to make sure it works when the data is already in the cache
+function run_test_20() {
+ restartManager();
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test20_install_ended() {
+ do_check_eq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_21);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that installing an add-on that is already pending install cancels the
+// first install
+function run_test_21() {
+ restartManager();
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
+
+ installAllFiles([do_get_addon("test_install2_1")], function() {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled",
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallCancelled",
+ "onInstallEnded",
+ ], check_test_21);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.install();
+ }, "application/x-xpinstall");
+ });
+ });
+}
+
+function check_test_21(aInstall) {
+ AddonManager.getAllInstalls(callback_soon(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], aInstall);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled",
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2, null);
+
+ run_test_22();
+ });
+ }));
+}
+
+// Tests that an install can be restarted after being cancelled
+function run_test_22() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_22);
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_22(aInstall) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_22);
+
+ aInstall.install();
+}
+
+function finish_test_22(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_23();
+}
+
+// Tests that an install can be restarted after being cancelled when a hash
+// was provided
+function run_test_23() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_23);
+ aInstall.install();
+ }, "application/x-xpinstall", do_get_addon_hash("test_install3"));
+}
+
+function check_test_23(aInstall) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_23);
+
+ aInstall.install();
+}
+
+function finish_test_23(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_24();
+}
+
+// Tests that an install with a bad hash can be restarted after it fails, though
+// it will only fail again
+function run_test_24() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadFailed",
+ ], check_test_24);
+ aInstall.install();
+ }, "application/x-xpinstall", "sha1:foo");
+}
+
+function check_test_24(aInstall) {
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadFailed"
+ ], run_test_25);
+
+ aInstall.install();
+}
+
+// Tests that installs with a hash for a local file work
+function run_test_25() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = Services.io.newFileURI(do_get_addon("test_install3")).spec;
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(aInstall.error, 0);
+
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_26();
+ }, "application/x-xpinstall", do_get_addon_hash("test_install3"));
+}
+
+function run_test_26() {
+ prepare_test({ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadCancelled"
+ ]);
+
+ let observerService = AM_Cc["@mozilla.org/network/http-activity-distributor;1"].
+ getService(AM_Ci.nsIHttpActivityDistributor);
+ observerService.addObserver({
+ observeActivity: function(aChannel, aType, aSubtype, aTimestamp, aSizeData,
+ aStringData) {
+ aChannel.QueryInterface(AM_Ci.nsIChannel);
+ // Wait for the final event for the redirected URL
+ if (aChannel.URI.spec != "http://localhost:" + gPort + "/addons/test_install1.xpi" ||
+ aType != AM_Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION ||
+ aSubtype != AM_Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
+ return;
+
+ // Request should have been cancelled
+ do_check_eq(aChannel.status, Components.results.NS_BINDING_ABORTED);
+
+ observerService.removeObserver(this);
+
+ run_test_27();
+ }
+ });
+
+ let url = "http://localhost:" + gPort + "/redirect?/addons/test_install1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onDownloadProgress: function(aDownloadProgressInstall) {
+ aDownloadProgressInstall.cancel();
+ }
+ });
+
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+
+// Tests that an install can be restarted during onDownloadCancelled after being
+// cancelled in mid-download
+function run_test_27() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ aInstall.addListener({
+ onDownloadProgress: function() {
+ aInstall.removeListener(this);
+ aInstall.cancel();
+ }
+ });
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadCancelled",
+ ], check_test_27);
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_27(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_27);
+
+ let file = aInstall.file;
+ aInstall.install();
+ do_check_neq(file.path, aInstall.file.path);
+ do_check_false(file.exists());
+}
+
+function finish_test_27(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_28();
+}
+
+// Tests that an install that isn't strictly compatible and has
+// binary components correctly has appDisabled set (see bug 702868).
+function run_test_28() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install5.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 5");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted"
+ ], check_test_28);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 5", null, "1.0");
+}
+
+function check_test_28(install) {
+ ensure_test_completed();
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 5");
+ do_check_eq(install.state, AddonManager.STATE_INSTALLING);
+ do_check_eq(install.existingAddon, null);
+ do_check_false(install.addon.isCompatible);
+ do_check_true(install.addon.appDisabled);
+
+ prepare_test({}, [
+ "onInstallCancelled"
+ ], finish_test_28);
+ return false;
+}
+
+function finish_test_28(install) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ], run_test_29);
+
+ install.cancel();
+}
+
+// Tests that an install with a matching compatibility override has appDisabled
+// set correctly.
+function run_test_29() {
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:" + gPort + "/addons/test_install6.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Addon Test 6");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], check_test_29);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Addon Test 6", null, "1.0");
+}
+
+function check_test_29(install) {
+ // ensure_test_completed();
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_neq(install.addon, null);
+ do_check_false(install.addon.isCompatible);
+ do_check_true(install.addon.appDisabled);
+
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ], run_test_30);
+ install.cancel();
+ return false;
+}
+
+// Tests that a multi-package XPI with no add-ons inside shows up as a
+// corrupt file
+function run_test_30() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+ do_check_eq(install.linkedInstalls, null);
+
+ run_test_31();
+ });
+}
+
+// Tests that a multi-package XPI with no valid add-ons inside shows up as a
+// corrupt file
+function run_test_31() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install8"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+ do_check_eq(install.linkedInstalls, null);
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_from_sources.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_from_sources.js
new file mode 100644
index 000000000..18bb7d74e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_from_sources.js
@@ -0,0 +1,80 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+BootstrapMonitor.init();
+
+// Partial list of bootstrap reasons from XPIProvider.jsm
+const BOOTSTRAP_REASONS = {
+ ADDON_INSTALL: 5,
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+};
+
+// Install an unsigned add-on with no existing add-on present.
+// Restart and make sure it is still around.
+add_task(function*() {
+ let extInstallCalled = false;
+ AddonManager.addInstallListener({
+ onExternalInstall: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ extInstallCalled = true;
+ },
+ });
+
+ let installingCalled = false;
+ let installedCalled = false;
+ AddonManager.addAddonListener({
+ onInstalling: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installingCalled = true;
+ },
+ onInstalled: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installedCalled = true;
+ },
+ onInstallStarted: (aInstall) => {
+ do_throw("onInstallStarted called unexpectedly");
+ }
+ });
+
+ yield AddonManager.installAddonFromSources(do_get_file("data/from_sources/"));
+
+ do_check_true(extInstallCalled);
+ do_check_true(installingCalled);
+ do_check_true(installedCalled);
+
+ let install = BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ yield promiseRestartManager();
+
+ install = BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ yield promiseRestartManager();
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js
new file mode 100644
index 000000000..70f91c560
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_icons.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// use httpserver to find an available port
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+
+var addon_url = "http://localhost:" + gPort + "/test.xpi";
+var icon32_url = "http://localhost:" + gPort + "/icon.png";
+var icon64_url = "http://localhost:" + gPort + "/icon64.png";
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+
+ test_1();
+}
+
+function test_1() {
+ AddonManager.getInstallForURL(addon_url, function(aInstall) {
+ do_check_eq(aInstall.iconURL, null);
+ do_check_neq(aInstall.icons, null);
+ do_check_eq(aInstall.icons[32], undefined);
+ do_check_eq(aInstall.icons[64], undefined);
+ test_2();
+ }, "application/x-xpinstall", null, null, null, null, null);
+}
+
+function test_2() {
+ AddonManager.getInstallForURL(addon_url, function(aInstall) {
+ do_check_eq(aInstall.iconURL, icon32_url);
+ do_check_neq(aInstall.icons, null);
+ do_check_eq(aInstall.icons[32], icon32_url);
+ do_check_eq(aInstall.icons[64], undefined);
+ test_3();
+ }, "application/x-xpinstall", null, null, icon32_url, null, null);
+}
+
+function test_3() {
+ AddonManager.getInstallForURL(addon_url, function(aInstall) {
+ do_check_eq(aInstall.iconURL, icon32_url);
+ do_check_neq(aInstall.icons, null);
+ do_check_eq(aInstall.icons[32], icon32_url);
+ do_check_eq(aInstall.icons[64], undefined);
+ test_4();
+ }, "application/x-xpinstall", null, null, { "32": icon32_url }, null, null);
+}
+
+function test_4() {
+ AddonManager.getInstallForURL(addon_url, function(aInstall) {
+ do_check_eq(aInstall.iconURL, icon32_url);
+ do_check_neq(aInstall.icons, null);
+ do_check_eq(aInstall.icons[32], icon32_url);
+ do_check_eq(aInstall.icons[64], icon64_url);
+ do_execute_soon(do_test_finished);
+ }, "application/x-xpinstall", null, null, { "32": icon32_url, "64": icon64_url }, null, null);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
new file mode 100644
index 000000000..77f806ba2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_install_strictcompat.js
@@ -0,0 +1,1726 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons can be installed from XPI files
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+// install.rdf size, icon.png, icon64.png size
+const ADDON1_SIZE = 705 + 16 + 16;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+var testserver;
+var gInstallDate;
+var gInstall = null;
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ // Make sure we only register once despite multiple calls
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+
+ // Create and configure the HTTP server.
+ testserver = new HttpServer();
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerPathHandler("/redirect", function(aRequest, aResponse) {
+ aResponse.setStatusLine(null, 301, "Moved Permanently");
+ let url = aRequest.host + ":" + aRequest.port + aRequest.queryString;
+ aResponse.setHeader("Location", "http://" + url);
+ });
+ testserver.start(4444);
+
+ do_test_pending();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+// Checks that an install from a local file proceeds as expected
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install1"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.type, "extension");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_eq(install.addon.install, install);
+ do_check_eq(install.addon.size, ADDON1_SIZE);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+ let file = do_get_addon("test_install1");
+ let uri = Services.io.newFileURI(file).spec;
+ do_check_eq(install.addon.getResourceURI("install.rdf").spec, "jar:" + uri + "!/install.rdf");
+ do_check_eq(install.addon.iconURL, "jar:" + uri + "!/icon.png");
+ do_check_eq(install.addon.icon64URL, "jar:" + uri + "!/icon64.png");
+ do_check_eq(install.iconURL, null);
+
+ do_check_eq(install.sourceURI.spec, uri);
+ do_check_eq(install.addon.sourceURI.spec, uri);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ AddonManager.getInstallsByTypes(["foo"], function(fooInstalls) {
+ do_check_eq(fooInstalls.length, 0);
+
+ AddonManager.getInstallsByTypes(["extension"], function(extensionInstalls) {
+ do_check_eq(extensionInstalls.length, 1);
+ do_check_eq(extensionInstalls[0], install);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_1);
+ install.install();
+ });
+ });
+ });
+ });
+}
+
+function check_test_1() {
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(olda1) {
+ do_check_eq(olda1, null);
+
+ AddonManager.getAddonsWithOperationsByTypes(null, callback_soon(function(pendingAddons) {
+ do_check_eq(pendingAddons.length, 1);
+ do_check_eq(pendingAddons[0].id, "addon1@tests.mozilla.org");
+ let uri = NetUtil.newURI(pendingAddons[0].iconURL);
+ if (uri instanceof AM_Ci.nsIJARURI) {
+ let jarURI = uri.QueryInterface(AM_Ci.nsIJARURI);
+ let archiveURI = jarURI.JARFile;
+ let archiveFile = archiveURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ let zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].
+ createInstance(Ci.nsIZipReader);
+ try {
+ zipReader.open(archiveFile);
+ do_check_true(zipReader.hasEntry(jarURI.JAREntry));
+ }
+ finally {
+ zipReader.close();
+ }
+ }
+ else {
+ let iconFile = uri.QueryInterface(AM_Ci.nsIFileURL).file;
+ do_check_true(iconFile.exists());
+ }
+
+ // Make the pending install have a sensible date
+ let updateDate = Date.now();
+ let extURI = pendingAddons[0].getResourceURI("");
+ let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ setExtensionModifiedTime(ext, updateDate);
+
+ // The pending add-on cannot be disabled or enabled.
+ do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_false(hasFlag(pendingAddons[0].permissions, AddonManager.PERM_CAN_DISABLE));
+
+ restartManager();
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls, 0);
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.type, "extension");
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.name, "Test 1");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(do_get_addon("test_install1").exists());
+ do_check_in_crash_annotation(a1.id, a1.version);
+ do_check_eq(a1.size, ADDON1_SIZE);
+
+ do_check_eq(a1.sourceURI.spec,
+ Services.io.newFileURI(do_get_addon("test_install1")).spec);
+ let difference = a1.installDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on install time was out by " + difference + "ms");
+
+ difference = a1.updateDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on update time was out by " + difference + "ms");
+
+ do_check_true(a1.hasResource("install.rdf"));
+ do_check_false(a1.hasResource("foo.bar"));
+
+ let root_uri = do_get_addon_root_uri(profileDir, "addon1@tests.mozilla.org");
+ do_check_eq(a1.getResourceURI("install.rdf").spec, root_uri + "install.rdf");
+ do_check_eq(a1.iconURL, root_uri + "icon.png");
+ do_check_eq(a1.icon64URL, root_uri + "icon64.png");
+
+ a1.uninstall();
+ do_execute_soon(function() { run_test_2(a1) });
+ });
+ });
+ }));
+ });
+}
+
+// Tests that an install from a url downloads.
+function run_test_2(aAddon) {
+ let { id, version } = aAddon;
+ restartManager();
+ do_check_not_in_crash_annotation(id, version);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test 2");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.iconURL, null);
+ do_check_eq(install.sourceURI.spec, url);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_2);
+
+ install.addListener({
+ onDownloadProgress: function() {
+ do_execute_soon(function() {
+ Components.utils.forceGC();
+ });
+ }
+ });
+
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 2", null, "1.0");
+}
+
+function check_test_2(install) {
+ ensure_test_completed();
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.name, "Real Test 2");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.addon.install, install);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+ do_check_eq(install.iconURL, null);
+
+ // Pause the install here and start it again in run_test_3
+ do_execute_soon(function() { run_test_3(install); });
+ return false;
+}
+
+// Tests that the downloaded XPI installs ok
+function run_test_3(install) {
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_3);
+ install.install();
+}
+
+function check_test_3(aInstall) {
+ // Make the pending install have a sensible date
+ let updateDate = Date.now();
+ let extURI = aInstall.addon.getResourceURI("");
+ let ext = extURI.QueryInterface(AM_Ci.nsIFileURL).file;
+ setExtensionModifiedTime(ext, updateDate);
+
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+ do_check_eq(olda2, null);
+ restartManager();
+
+ AddonManager.getAllInstalls(function(installs) {
+ do_check_eq(installs, 0);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_eq(a2.type, "extension");
+ do_check_eq(a2.version, "2.0");
+ do_check_eq(a2.name, "Real Test 2");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(do_get_addon("test_install2_1").exists());
+ do_check_in_crash_annotation(a2.id, a2.version);
+ do_check_eq(a2.sourceURI.spec,
+ "http://localhost:4444/addons/test_install2_1.xpi");
+
+ let difference = a2.installDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on install time was out by " + difference + "ms");
+
+ difference = a2.updateDate.getTime() - updateDate;
+ if (Math.abs(difference) > MAX_TIME_DIFFERENCE)
+ do_throw("Add-on update time was out by " + difference + "ms");
+
+ gInstallDate = a2.installDate.getTime();
+
+ run_test_4();
+ });
+ });
+ }));
+}
+
+// Tests that installing a new version of an existing add-on works
+function run_test_4() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Test 3");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+ do_check_eq(install.existingAddon, null);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_4);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 3", null, "3.0");
+}
+
+function check_test_4(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Real Test 3");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_neq(install.existingAddon);
+ do_check_eq(install.existingAddon.id, "addon2@tests.mozilla.org");
+ do_check_eq(install.addon.install, install);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ run_test_5();
+ // Installation will continue when there is nothing returned.
+}
+
+// Continue installing the new version
+function run_test_5() {
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_5);
+}
+
+function check_test_5(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(olda2) {
+ do_check_neq(olda2, null);
+ do_check_true(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+
+ AddonManager.getInstallsByTypes(null, callback_soon(function(installs) {
+ do_check_eq(installs.length, 1);
+ do_check_eq(installs[0].addon, olda2.pendingUpgrade);
+ restartManager();
+
+ AddonManager.getInstallsByTypes(null, function(installs2) {
+ do_check_eq(installs2.length, 0);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_eq(a2.type, "extension");
+ do_check_eq(a2.version, "3.0");
+ do_check_eq(a2.name, "Real Test 3");
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(do_get_addon("test_install2_2").exists());
+ do_check_in_crash_annotation(a2.id, a2.version);
+ do_check_eq(a2.sourceURI.spec,
+ "http://localhost:4444/addons/test_install2_2.xpi");
+
+ do_check_eq(a2.installDate.getTime(), gInstallDate);
+ // Update date should be later (or the same if this test is too fast)
+ do_check_true(a2.installDate <= a2.updateDate);
+
+ a2.uninstall();
+ do_execute_soon(run_test_6);
+ });
+ });
+ }));
+ });
+}
+
+// Tests that an install that requires a compatibility update works
+function run_test_6() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_6);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_6(install) {
+ ensure_test_completed();
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.existingAddon, null);
+ do_check_false(install.addon.appDisabled);
+ run_test_7();
+ return true;
+}
+
+// Continue the install
+function run_test_7() {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_7);
+}
+
+function check_test_7() {
+ ensure_test_completed();
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", callback_soon(function(olda3) {
+ do_check_eq(olda3, null);
+ restartManager();
+
+ AddonManager.getAllInstalls(function(installs) {
+ do_check_eq(installs, 0);
+
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_eq(a3.type, "extension");
+ do_check_eq(a3.version, "1.0");
+ do_check_eq(a3.name, "Real Test 4");
+ do_check_true(a3.isActive);
+ do_check_false(a3.appDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(do_get_addon("test_install3").exists());
+ a3.uninstall();
+ do_execute_soon(run_test_8);
+ });
+ });
+ }));
+}
+
+function run_test_8() {
+ restartManager();
+
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install3"), function(install) {
+ do_check_true(install.addon.isCompatible);
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_8));
+ install.install();
+ });
+}
+
+function check_test_8() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_eq(a3.type, "extension");
+ do_check_eq(a3.version, "1.0");
+ do_check_eq(a3.name, "Real Test 4");
+ do_check_true(a3.isActive);
+ do_check_false(a3.appDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(do_get_addon("test_install3").exists());
+ a3.uninstall();
+ do_execute_soon(run_test_9);
+ });
+}
+
+// Test that after cancelling a download it is removed from the active installs
+function run_test_9() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_9);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_9(install) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ], function() {
+ let file = install.file;
+
+ // Allow the file removal to complete
+ do_execute_soon(function() {
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 0);
+ do_check_false(file.exists());
+
+ run_test_10();
+ });
+ });
+ });
+
+ install.cancel();
+}
+
+// Tests that after cancelling a pending install it is removed from the active
+// installs
+function run_test_10() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Real Test 4");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getInstallsByTypes(null, function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], check_test_10);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Real Test 4", null, "1.0");
+}
+
+function check_test_10(install) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ install.cancel();
+
+ ensure_test_completed();
+
+ AddonManager.getAllInstalls(callback_soon(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 0);
+
+ restartManager();
+
+ // Check that the install did not complete
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_eq(a3, null);
+
+ run_test_11();
+ });
+ }));
+}
+
+// Tests that a multi-package install shows up as multiple installs with the
+// correct sourceURI.
+function run_test_11() {
+ prepare_test({ }, [
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install4"), function(install) {
+ ensure_test_completed();
+ do_check_neq(install, null);
+ do_check_neq(install.linkedInstalls, null);
+ do_check_eq(install.linkedInstalls.length, 5);
+
+ // Might be in any order so sort them based on ID
+ let installs = [install].concat(install.linkedInstalls);
+ installs.sort(function(a, b) {
+ if (a.state != b.state) {
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 1;
+ else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return -1;
+ }
+
+ // Don't care what order the failed installs show up in
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 0;
+
+ if (a.addon.id < b.addon.id)
+ return -1;
+ if (a.addon.id > b.addon.id)
+ return 1;
+ return 0;
+ });
+
+ // Comes from addon4.xpi and is made compatible by an update check
+ do_check_eq(installs[0].sourceURI, install.sourceURI);
+ do_check_eq(installs[0].addon.id, "addon4@tests.mozilla.org");
+ do_check_false(installs[0].addon.appDisabled);
+ do_check_eq(installs[0].version, "1.0");
+ do_check_eq(installs[0].name, "Multi Test 1");
+ do_check_eq(installs[0].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[0].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon5.jar and is compatible by default
+ do_check_eq(installs[1].sourceURI, install.sourceURI);
+ do_check_eq(installs[1].addon.id, "addon5@tests.mozilla.org");
+ do_check_false(installs[1].addon.appDisabled);
+ do_check_eq(installs[1].version, "3.0");
+ do_check_eq(installs[1].name, "Multi Test 2");
+ do_check_eq(installs[1].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[1].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon6.xpi and is incompatible
+ do_check_eq(installs[2].sourceURI, install.sourceURI);
+ do_check_eq(installs[2].addon.id, "addon6@tests.mozilla.org");
+ do_check_true(installs[2].addon.appDisabled);
+ do_check_eq(installs[2].version, "2.0");
+ do_check_eq(installs[2].name, "Multi Test 3");
+ do_check_eq(installs[2].state, AddonManager.STATE_DOWNLOADED);
+ do_check_false(hasFlag(installs[2].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ // Comes from addon7.jar and is made compatible by an update check
+ do_check_eq(installs[3].sourceURI, install.sourceURI);
+ do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
+ do_check_false(installs[3].addon.appDisabled);
+ do_check_eq(installs[3].version, "5.0");
+ do_check_eq(installs[3].name, "Multi Test 4");
+ do_check_eq(installs[3].state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(installs[3].addon.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 4);
+
+ prepare_test({
+ "addon4@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon6@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, {
+ "addon4@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ]
+ }, callback_soon(check_test_11));
+
+ installs[0].install();
+ installs[1].install();
+ installs[3].install();
+
+ // Note that we install addon6 last. Since it doesn't need a restart to
+ // install it completes asynchronously which would otherwise make the
+ // onInstallStarted/onInstallEnded events go out of sequence unless this
+ // is the last install operation
+ installs[2].install();
+ });
+ });
+}
+
+function check_test_11() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a4, a5, a6, a7]) {
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+ do_check_neq(a6, null);
+ do_check_neq(a7, null);
+
+ a4.uninstall();
+ a5.uninstall();
+ a6.uninstall();
+ a7.uninstall();
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+// Same as test 11 but for a remote XPI
+function run_test_12() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall",
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install4.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ gInstall = install;
+
+ ensure_test_completed();
+ do_check_neq(install, null);
+ do_check_eq(install.linkedInstalls, null);
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({
+ "addon4@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstalling"
+ ],
+ "addon6@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, {
+ "NO_ID": [
+ "onDownloadStarted",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onNewInstall",
+ "onDownloadEnded"
+ ],
+ "addon4@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon5@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon6@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ],
+ "addon7@tests.mozilla.org": [
+ "onInstallStarted",
+ "onInstallEnded"
+ ]
+ }, callback_soon(check_test_12));
+ install.install();
+ }, "application/x-xpinstall", null, "Multi Test 4");
+}
+
+function check_test_12() {
+ do_check_eq(gInstall.linkedInstalls.length, 5);
+
+ // Might be in any order so sort them based on ID
+ let installs = [gInstall].concat(gInstall.linkedInstalls);
+ installs.sort(function(a, b) {
+ if (a.state != b.state) {
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 1;
+ else if (b.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return -1;
+ }
+
+ // Don't care what order the failed installs show up in
+ if (a.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ return 0;
+
+ if (a.addon.id < b.addon.id)
+ return -1;
+ if (a.addon.id > b.addon.id)
+ return 1;
+ return 0;
+ });
+
+ // Comes from addon4.xpi and is made compatible by an update check
+ do_check_eq(installs[0].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[0].addon.id, "addon4@tests.mozilla.org");
+ do_check_false(installs[0].addon.appDisabled);
+ do_check_eq(installs[0].version, "1.0");
+ do_check_eq(installs[0].name, "Multi Test 1");
+ do_check_eq(installs[0].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon5.jar and is compatible by default
+ do_check_eq(installs[1].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[1].addon.id, "addon5@tests.mozilla.org");
+ do_check_false(installs[1].addon.appDisabled);
+ do_check_eq(installs[1].version, "3.0");
+ do_check_eq(installs[1].name, "Multi Test 2");
+ do_check_eq(installs[1].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon6.xpi and is incompatible
+ do_check_eq(installs[2].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[2].addon.id, "addon6@tests.mozilla.org");
+ do_check_true(installs[2].addon.appDisabled);
+ do_check_eq(installs[2].version, "2.0");
+ do_check_eq(installs[2].name, "Multi Test 3");
+ do_check_eq(installs[2].state, AddonManager.STATE_INSTALLED);
+
+ // Comes from addon7.jar and is made compatible by an update check
+ do_check_eq(installs[3].sourceURI, gInstall.sourceURI);
+ do_check_eq(installs[3].addon.id, "addon7@tests.mozilla.org");
+ do_check_false(installs[3].addon.appDisabled);
+ do_check_eq(installs[3].version, "5.0");
+ do_check_eq(installs[3].name, "Multi Test 4");
+ do_check_eq(installs[3].state, AddonManager.STATE_INSTALLED);
+
+ do_check_eq(installs[4].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[4].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ do_check_eq(installs[5].state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(installs[5].error, AddonManager.ERROR_CORRUPT_FILE);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a4, a5, a6, a7]) {
+ do_check_neq(a4, null);
+ do_check_neq(a5, null);
+ do_check_neq(a6, null);
+ do_check_neq(a7, null);
+
+ a4.uninstall();
+ a5.uninstall();
+ a6.uninstall();
+ a7.uninstall();
+
+ do_execute_soon(run_test_13);
+ });
+}
+
+
+// Tests that cancelling an upgrade leaves the original add-on's pendingOperations
+// correct
+function run_test_13() {
+ restartManager();
+
+ installAllFiles([do_get_addon("test_install2_1")], function() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Test 3");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+
+ AddonManager.getAllInstalls(function(activeInstalls) {
+ do_check_eq(activeInstalls.length, 1);
+ do_check_eq(activeInstalls[0], install);
+ do_check_eq(install.existingAddon, null);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_13);
+ install.install();
+ });
+ }, "application/x-xpinstall", null, "Test 3", null, "3.0");
+ });
+}
+
+function check_test_13(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.version, "3.0");
+ do_check_eq(install.name, "Real Test 3");
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_neq(install.existingAddon, null);
+ do_check_eq(install.existingAddon.id, "addon2@tests.mozilla.org");
+ do_check_eq(install.addon.install, install);
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(olda2) {
+ do_check_neq(olda2, null);
+ do_check_true(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+ do_check_eq(olda2.pendingUpgrade, install.addon);
+
+ do_check_true(hasFlag(install.addon.pendingOperations,
+ AddonManager.PENDING_INSTALL));
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled",
+ ]);
+
+ install.cancel();
+
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ do_check_false(hasFlag(olda2.pendingOperations, AddonManager.PENDING_UPGRADE));
+ do_check_eq(olda2.pendingUpgrade, null);
+
+ restartManager();
+
+ // Check that the upgrade did not complete
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.version, "2.0");
+
+ a2.uninstall();
+
+ do_execute_soon(run_test_14);
+ });
+ }));
+}
+
+// Check that cancelling the install from onDownloadStarted actually cancels it
+function run_test_14() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.file, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted"
+ ], check_test_14);
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_14(install) {
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ], function() {
+ let file = install.file;
+
+ install.addListener({
+ onDownloadProgress: function() {
+ do_throw("Download should not have continued");
+ },
+ onDownloadEnded: function() {
+ do_throw("Download should not have continued");
+ }
+ });
+
+ // Allow the listener to return to see if it continues downloading. The
+ // The listener only really tests if we give it time to see progress, the
+ // file check isn't ideal either
+ do_execute_soon(function() {
+ do_check_false(file.exists());
+
+ run_test_15();
+ });
+ });
+
+ // Wait for the channel to be ready to cancel
+ do_execute_soon(function() {
+ install.cancel();
+ });
+}
+
+// Checks that cancelling the install from onDownloadEnded actually cancels it
+function run_test_15() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(install) {
+ ensure_test_completed();
+
+ do_check_eq(install.file, null);
+
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], check_test_15);
+ install.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_15(install) {
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ]);
+
+ install.cancel();
+
+ ensure_test_completed();
+
+ install.addListener({
+ onInstallStarted: function() {
+ do_throw("Install should not have continued");
+ }
+ });
+
+ // Allow the listener to return to see if it starts installing
+ do_execute_soon(run_test_16);
+}
+
+// Verify that the userDisabled value carries over to the upgrade by default
+function run_test_16() {
+ restartManager();
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall.addon.userDisabled);
+ aInstall.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function test16_install1() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+
+ let url_2 = "http://localhost:4444/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function test16_install2() {
+ do_check_true(aInstall_2.addon.userDisabled);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_17);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that changing the userDisabled value before onInstallEnded works
+function run_test_17() {
+ restartManager();
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function() {
+ do_check_false(aInstall.addon.userDisabled);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.isActive);
+
+ let url_2 = "http://localhost:4444/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall_2.addon.userDisabled);
+ aInstall_2.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_true(a2_2.userDisabled);
+ do_check_false(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_18);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that changing the userDisabled value before onInstallEnded works
+function run_test_18() {
+ restartManager();
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallStarted: function() {
+ do_check_false(aInstall.addon.userDisabled);
+ aInstall.addon.userDisabled = true;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function test18_install1() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.isActive);
+
+ let url_2 = "http://localhost:4444/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallStarted: function() {
+ do_check_true(aInstall_2.addon.userDisabled);
+ aInstall_2.addon.userDisabled = false;
+ },
+
+ onInstallEnded: function() {
+ do_execute_soon(function test18_install2() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2_2) {
+ do_check_false(a2_2.userDisabled);
+ do_check_true(a2_2.isActive);
+
+ a2_2.uninstall();
+ do_execute_soon(run_test_18_1);
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+
+// Checks that metadata is not stored if the pref is set to false
+function run_test_18_1() {
+ restartManager();
+
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS,
+ "http://localhost:4444/data/test_install.xml");
+
+ Services.prefs.setBoolPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", false);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test18_install() {
+ do_check_neq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_19);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Checks that metadata is downloaded for new installs and is visible before and
+// after restart
+function run_test_19() {
+ restartManager();
+ Services.prefs.setBoolPref("extensions.addon2@tests.mozilla.org.getAddons.cache.enabled", true);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test19_install() {
+ do_check_eq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_20);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Do the same again to make sure it works when the data is already in the cache
+function run_test_20() {
+ restartManager();
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function(unused, aAddon) {
+ do_execute_soon(function test20_install() {
+ do_check_eq(aAddon.fullDescription, "Repository description");
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2.fullDescription, "Repository description");
+
+ a2.uninstall();
+ do_execute_soon(run_test_21);
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+// Verify that installing an add-on that is already pending install cancels the
+// first install
+function run_test_21() {
+ restartManager();
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", false);
+
+ installAllFiles([do_get_addon("test_install2_1")], function() {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled",
+ "onInstalling"
+ ]
+ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallCancelled",
+ "onInstallEnded",
+ ], check_test_21);
+
+ let url = "http://localhost:4444/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.install();
+ }, "application/x-xpinstall");
+ });
+ });
+}
+
+function check_test_21(aInstall) {
+ AddonManager.getAllInstalls(callback_soon(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], aInstall);
+
+ prepare_test({
+ "addon2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled",
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_eq(a2, null);
+
+ run_test_22();
+ });
+ }));
+}
+
+// Tests that an install can be restarted after being cancelled
+function run_test_22() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_22);
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_22(aInstall) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_22);
+
+ aInstall.install();
+}
+
+function finish_test_22(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_23();
+}
+
+// Tests that an install can be restarted after being cancelled when a hash
+// was provided
+function run_test_23() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_23);
+ aInstall.install();
+ }, "application/x-xpinstall", do_get_addon_hash("test_install3"));
+}
+
+function check_test_23(aInstall) {
+ prepare_test({}, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_23);
+
+ aInstall.install();
+}
+
+function finish_test_23(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_24();
+}
+
+// Tests that an install with a bad hash can be restarted after it fails, though
+// it will only fail again
+function run_test_24() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadFailed",
+ ], check_test_24);
+ aInstall.install();
+ }, "application/x-xpinstall", "sha1:foo");
+}
+
+function check_test_24(aInstall) {
+ prepare_test({ }, [
+ "onDownloadStarted",
+ "onDownloadFailed"
+ ], run_test_25);
+
+ aInstall.install();
+}
+
+// Tests that installs with a hash for a local file work
+function run_test_25() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = Services.io.newFileURI(do_get_addon("test_install3")).spec;
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(aInstall.error, 0);
+
+ prepare_test({ }, [
+ "onDownloadCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_26();
+ }, "application/x-xpinstall", do_get_addon_hash("test_install3"));
+}
+
+function run_test_26() {
+ prepare_test({ }, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadCancelled"
+ ]);
+
+ let observerService = AM_Cc["@mozilla.org/network/http-activity-distributor;1"].
+ getService(AM_Ci.nsIHttpActivityDistributor);
+ observerService.addObserver({
+ observeActivity: function(aChannel, aType, aSubtype, aTimestamp, aSizeData,
+ aStringData) {
+ aChannel.QueryInterface(AM_Ci.nsIChannel);
+ // Wait for the final event for the redirected URL
+ if (aChannel.URI.spec != "http://localhost:4444/addons/test_install1.xpi" ||
+ aType != AM_Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION ||
+ aSubtype != AM_Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE)
+ return;
+
+ // Request should have been cancelled
+ do_check_eq(aChannel.status, Components.results.NS_BINDING_ABORTED);
+
+ observerService.removeObserver(this);
+
+ run_test_27();
+ }
+ });
+
+ let url = "http://localhost:4444/redirect?/addons/test_install1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onDownloadProgress: function(aDownloadProgressInstall) {
+ aDownloadProgressInstall.cancel();
+ }
+ });
+
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+
+// Tests that an install can be restarted during onDownloadCancelled after being
+// cancelled in mid-download
+function run_test_27() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ let url = "http://localhost:4444/addons/test_install3.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ ensure_test_completed();
+
+ do_check_neq(aInstall, null);
+ do_check_eq(aInstall.state, AddonManager.STATE_AVAILABLE);
+
+ aInstall.addListener({
+ onDownloadProgress: function() {
+ aInstall.removeListener(this);
+ aInstall.cancel();
+ }
+ });
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadCancelled",
+ ], check_test_27);
+ aInstall.install();
+ }, "application/x-xpinstall");
+}
+
+function check_test_27(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ "onInstallStarted",
+ "onInstallEnded"
+ ], finish_test_27);
+
+ let file = aInstall.file;
+ aInstall.install();
+ do_check_neq(file.path, aInstall.file.path);
+ do_check_false(file.exists());
+}
+
+function finish_test_27(aInstall) {
+ prepare_test({
+ "addon3@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ }, [
+ "onInstallCancelled"
+ ]);
+
+ aInstall.cancel();
+
+ ensure_test_completed();
+
+ run_test_30();
+}
+
+// Tests that a multi-package XPI with no add-ons inside shows up as a
+// corrupt file
+function run_test_30() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install7"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+ do_check_eq(install.linkedInstalls, null);
+
+ run_test_31();
+ });
+}
+
+// Tests that a multi-package XPI with no valid add-ons inside shows up as a
+// corrupt file
+function run_test_31() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install8"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+ do_check_eq(install.linkedInstalls, null);
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js b/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js
new file mode 100644
index 000000000..87f2856b0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_isDebuggable.js
@@ -0,0 +1,36 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var ADDONS = [
+ "test_bootstrap2_1", // restartless addon
+ "test_bootstrap1_4", // old-school addon
+ "test_jetpack" // sdk addon
+];
+
+var IDS = [
+ "bootstrap1@tests.mozilla.org",
+ "bootstrap2@tests.mozilla.org",
+ "jetpack@tests.mozilla.org"
+];
+
+function run_test() {
+ do_test_pending();
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ startupManager();
+ AddonManager.checkCompatibility = false;
+
+ installAllFiles(ADDONS.map(do_get_addon), function () {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(IDS, function([a1, a2, a3]) {
+ do_check_eq(a1.isDebuggable, false);
+ do_check_eq(a2.isDebuggable, true);
+ do_check_eq(a3.isDebuggable, true);
+ do_test_finished();
+ });
+ }, true);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js b/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
new file mode 100644
index 000000000..6222398a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_isReady.js
@@ -0,0 +1,49 @@
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* () {
+ equal(AddonManager.isReady, false, "isReady should be false before startup");
+
+ let gotStartupEvent = false;
+ let gotShutdownEvent = false;
+ let listener = {
+ onStartup() {
+ gotStartupEvent = true;
+ },
+ onShutdown() {
+ gotShutdownEvent = true;
+ },
+ };
+ AddonManager.addManagerListener(listener);
+
+ do_print("Starting manager...");
+ startupManager();
+ equal(AddonManager.isReady, true, "isReady should be true after startup");
+ equal(gotStartupEvent, true, "Should have seen onStartup event after startup");
+ equal(gotShutdownEvent, false, "Should not have seen onShutdown event before shutdown");
+
+ gotStartupEvent = false;
+ gotShutdownEvent = false;
+
+ do_print("Shutting down manager...");
+ let shutdownPromise = promiseShutdownManager();
+ equal(AddonManager.isReady, false, "isReady should be false when shutdown commences");
+ yield shutdownPromise;
+
+ equal(AddonManager.isReady, false, "isReady should be false after shutdown");
+ equal(gotStartupEvent, false, "Should not have seen onStartup event after shutdown");
+ equal(gotShutdownEvent, true, "Should have seen onShutdown event after shutdown");
+
+ AddonManager.addManagerListener(listener);
+ gotStartupEvent = false;
+ gotShutdownEvent = false;
+
+ do_print("Starting manager again...");
+ startupManager();
+ equal(AddonManager.isReady, true, "isReady should be true after repeat startup");
+ equal(gotStartupEvent, true, "Should have seen onStartup event after repeat startup");
+ equal(gotShutdownEvent, false, "Should not have seen onShutdown event before shutdown, following repeat startup");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js b/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js
new file mode 100644
index 000000000..adf789afb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_json_updatecheck.js
@@ -0,0 +1,372 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+"use strict";
+
+// This verifies that AddonUpdateChecker works correctly for JSON
+// update manifests, particularly for behavior which does not
+// cleanly overlap with RDF manifests.
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+const TOOLKIT_MINVERSION = "42.0a1";
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0a2", "42.0a2");
+
+Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
+
+let testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+
+let gUpdateManifests = {};
+
+function mapManifest(aPath, aManifestData) {
+ gUpdateManifests[aPath] = aManifestData;
+ testserver.registerPathHandler(aPath, serveManifest);
+}
+
+function serveManifest(request, response) {
+ let manifest = gUpdateManifests[request.path];
+
+ response.setHeader("Content-Type", manifest.contentType, false);
+ response.write(manifest.data);
+}
+
+const extensionsDir = gProfD.clone();
+extensionsDir.append("extensions");
+
+
+function checkUpdates(aData) {
+ // Registers JSON update manifest for it with the testing server,
+ // checks for updates, and yields the list of updates on
+ // success.
+
+ let extension = aData.manifestExtension || "json";
+
+ let path = `/updates/${aData.id}.${extension}`;
+ let updateUrl = `http://localhost:${gPort}${path}`
+
+ let addonData = {};
+ if ("updates" in aData)
+ addonData.updates = aData.updates;
+
+ let manifestJSON = {
+ "addons": {
+ [aData.id]: addonData
+ }
+ };
+
+ mapManifest(path.replace(/\?.*/, ""),
+ { data: JSON.stringify(manifestJSON),
+ contentType: aData.contentType || "application/json" });
+
+
+ return new Promise((resolve, reject) => {
+ AddonUpdateChecker.checkForUpdates(aData.id, aData.updateKey, updateUrl, {
+ onUpdateCheckComplete: resolve,
+
+ onUpdateCheckError: function(status) {
+ reject(new Error("Update check failed with status " + status));
+ }
+ });
+ });
+}
+
+
+add_task(function* test_default_values() {
+ // Checks that the appropriate defaults are used for omitted values.
+
+ startupManager();
+
+ let updates = yield checkUpdates({
+ id: "updatecheck-defaults@tests.mozilla.org",
+ version: "0.1",
+ updates: [{
+ version: "0.2"
+ }]
+ });
+
+ equal(updates.length, 1);
+ let update = updates[0];
+
+ equal(update.targetApplications.length, 1);
+ let targetApp = update.targetApplications[0];
+
+ equal(targetApp.id, TOOLKIT_ID);
+ equal(targetApp.minVersion, TOOLKIT_MINVERSION);
+ equal(targetApp.maxVersion, "*");
+
+ equal(update.version, "0.2");
+ equal(update.multiprocessCompatible, true, "multiprocess_compatible flag");
+ equal(update.strictCompatibility, false, "inferred strictConpatibility flag");
+ equal(update.updateURL, null, "updateURL");
+ equal(update.updateHash, null, "updateHash");
+ equal(update.updateInfoURL, null, "updateInfoURL");
+
+ // If there's no applications property, we default to using one
+ // containing "gecko". If there is an applications property, but
+ // it doesn't contain "gecko", the update is skipped.
+ updates = yield checkUpdates({
+ id: "updatecheck-defaults@tests.mozilla.org",
+ version: "0.1",
+ updates: [{
+ version: "0.2",
+ applications: { "foo": {} }
+ }]
+ });
+
+ equal(updates.length, 0);
+
+ // Updates property is also optional. No updates, but also no error.
+ updates = yield checkUpdates({
+ id: "updatecheck-defaults@tests.mozilla.org",
+ version: "0.1",
+ });
+
+ equal(updates.length, 0);
+});
+
+
+add_task(function* test_explicit_values() {
+ // Checks that the appropriate explicit values are used when
+ // provided.
+
+ let updates = yield checkUpdates({
+ id: "updatecheck-explicit@tests.mozilla.org",
+ version: "0.1",
+ updates: [{
+ version: "0.2",
+ update_link: "https://example.com/foo.xpi",
+ update_hash: "sha256:0",
+ update_info_url: "https://example.com/update_info.html",
+ multiprocess_compatible: false,
+ applications: {
+ gecko: {
+ strict_min_version: "42.0a2.xpcshell",
+ strict_max_version: "43.xpcshell"
+ }
+ }
+ }]
+ });
+
+ equal(updates.length, 1);
+ let update = updates[0];
+
+ equal(update.targetApplications.length, 1);
+ let targetApp = update.targetApplications[0];
+
+ equal(targetApp.id, TOOLKIT_ID);
+ equal(targetApp.minVersion, "42.0a2.xpcshell");
+ equal(targetApp.maxVersion, "43.xpcshell");
+
+ equal(update.version, "0.2");
+ equal(update.multiprocessCompatible, false, "multiprocess_compatible flag");
+ equal(update.strictCompatibility, true, "inferred strictCompatibility flag");
+ equal(update.updateURL, "https://example.com/foo.xpi", "updateURL");
+ equal(update.updateHash, "sha256:0", "updateHash");
+ equal(update.updateInfoURL, "https://example.com/update_info.html", "updateInfoURL");
+});
+
+
+add_task(function* test_secure_hashes() {
+ // Checks that only secure hash functions are accepted for
+ // non-secure update URLs.
+
+ let hashFunctions = ["sha512",
+ "sha256",
+ "sha1",
+ "md5",
+ "md4",
+ "xxx"];
+
+ let updateItems = hashFunctions.map((hash, idx) => ({
+ version: `0.${idx}`,
+ update_link: `http://localhost:${gPort}/updates/${idx}-${hash}.xpi`,
+ update_hash: `${hash}:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a`,
+ }));
+
+ let { messages, result: updates } = yield promiseConsoleOutput(() => {
+ return checkUpdates({
+ id: "updatecheck-hashes@tests.mozilla.org",
+ version: "0.1",
+ updates: updateItems
+ });
+ });
+
+ equal(updates.length, hashFunctions.length);
+
+ updates = updates.filter(update => update.updateHash || update.updateURL);
+ equal(updates.length, 2, "expected number of update hashes were accepted");
+
+ ok(updates[0].updateHash.startsWith("sha512:"), "sha512 hash is present");
+ ok(updates[0].updateURL);
+
+ ok(updates[1].updateHash.startsWith("sha256:"), "sha256 hash is present");
+ ok(updates[1].updateURL);
+
+ messages = messages.filter(msg => /Update link.*not secure.*strong enough hash \(needs to be sha256 or sha512\)/.test(msg.message));
+ equal(messages.length, hashFunctions.length - 2, "insecure hashes generated the expected warning");
+});
+
+
+add_task(function* test_strict_compat() {
+ // Checks that strict compatibility is enabled for strict max
+ // versions other than "*", but not for advisory max versions.
+ // Also, ensure that strict max versions take precedence over
+ // advisory versions.
+
+ let { messages, result: updates } = yield promiseConsoleOutput(() => {
+ return checkUpdates({
+ id: "updatecheck-strict@tests.mozilla.org",
+ version: "0.1",
+ updates: [
+ { version: "0.2",
+ applications: { gecko: { strict_max_version: "*" } } },
+ { version: "0.3",
+ applications: { gecko: { strict_max_version: "43" } } },
+ { version: "0.4",
+ applications: { gecko: { advisory_max_version: "43" } } },
+ { version: "0.5",
+ applications: { gecko: { advisory_max_version: "43",
+ strict_max_version: "44" } } },
+ ]
+ });
+ });
+
+ equal(updates.length, 4, "all update items accepted");
+
+ equal(updates[0].targetApplications[0].maxVersion, "*");
+ equal(updates[0].strictCompatibility, false);
+
+ equal(updates[1].targetApplications[0].maxVersion, "43");
+ equal(updates[1].strictCompatibility, true);
+
+ equal(updates[2].targetApplications[0].maxVersion, "43");
+ equal(updates[2].strictCompatibility, false);
+
+ equal(updates[3].targetApplications[0].maxVersion, "44");
+ equal(updates[3].strictCompatibility, true);
+
+ messages = messages.filter(msg => /Ignoring 'advisory_max_version'.*'strict_max_version' also present/.test(msg.message));
+ equal(messages.length, 1, "mix of advisory_max_version and strict_max_version generated the expected warning");
+});
+
+
+add_task(function* test_update_url_security() {
+ // Checks that update links to privileged URLs are not accepted.
+
+ let { messages, result: updates } = yield promiseConsoleOutput(() => {
+ return checkUpdates({
+ id: "updatecheck-security@tests.mozilla.org",
+ version: "0.1",
+ updates: [
+ { version: "0.2",
+ update_link: "chrome://browser/content/browser.xul",
+ update_hash: "sha256:08ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" },
+ { version: "0.3",
+ update_link: "http://example.com/update.xpi",
+ update_hash: "sha256:18ac852190ecd81f40a514ea9299fe9143d9ab5e296b97e73fb2a314de49648a" },
+ ]
+ });
+ });
+
+ equal(updates.length, 2, "both updates were processed");
+ equal(updates[0].updateURL, null, "privileged update URL was removed");
+ equal(updates[1].updateURL, "http://example.com/update.xpi", "safe update URL was accepted");
+
+ messages = messages.filter(msg => /http:\/\/localhost.*\/updates\/.*may not load or link to chrome:/.test(msg.message));
+ equal(messages.length, 1, "privileged upate URL generated the expected console message");
+});
+
+
+add_task(function* test_no_update_key() {
+ // Checks that updates fail when an update key has been specified.
+
+ let { messages } = yield promiseConsoleOutput(function* () {
+ yield Assert.rejects(
+ checkUpdates({
+ id: "updatecheck-updatekey@tests.mozilla.org",
+ version: "0.1",
+ updateKey: "ayzzx=",
+ updates: [
+ { version: "0.2" },
+ { version: "0.3" },
+ ]
+ }),
+ null, "updated expected to fail");
+ });
+
+ messages = messages.filter(msg => /Update keys are not supported for JSON update manifests/.test(msg.message));
+ equal(messages.length, 1, "got expected update-key-unsupported error");
+});
+
+
+add_task(function* test_type_detection() {
+ // Checks that JSON update manifests are detected correctly
+ // regardless of extension or MIME type.
+
+ let tests = [
+ { contentType: "application/json",
+ extension: "json",
+ valid: true },
+ { contentType: "application/json",
+ extension: "php",
+ valid: true },
+ { contentType: "text/plain",
+ extension: "json",
+ valid: true },
+ { contentType: "application/octet-stream",
+ extension: "json",
+ valid: true },
+ { contentType: "text/plain",
+ extension: "json?foo=bar",
+ valid: true },
+ { contentType: "text/plain",
+ extension: "php",
+ valid: true },
+ { contentType: "text/plain",
+ extension: "rdf",
+ valid: true },
+ { contentType: "application/json",
+ extension: "rdf",
+ valid: true },
+ { contentType: "text/xml",
+ extension: "json",
+ valid: true },
+ { contentType: "application/rdf+xml",
+ extension: "json",
+ valid: true },
+ ];
+
+ for (let [i, test] of tests.entries()) {
+ let { messages } = yield promiseConsoleOutput(function *() {
+ let id = `updatecheck-typedetection-${i}@tests.mozilla.org`;
+ let updates;
+ try {
+ updates = yield checkUpdates({
+ id: id,
+ version: "0.1",
+ contentType: test.contentType,
+ manifestExtension: test.extension,
+ updates: [{ version: "0.2" }]
+ });
+ } catch (e) {
+ ok(!test.valid, "update manifest correctly detected as RDF");
+ return;
+ }
+
+ ok(test.valid, "update manifest correctly detected as JSON");
+ equal(updates.length, 1, "correct number of updates");
+ equal(updates[0].id, id, "update is for correct extension");
+ });
+
+ if (test.valid) {
+ // Make sure we don't get any XML parsing errors from the
+ // XMLHttpRequest machinery.
+ ok(!messages.some(msg => /not well-formed/.test(msg.message)),
+ "expect XMLHttpRequest not to attempt XML parsing");
+ }
+
+ messages = messages.filter(msg => /Update manifest was not valid XML/.test(msg.message));
+ equal(messages.length, !test.valid, "expected number of XML parsing errors");
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_langpack.js b/toolkit/mozapps/extensions/test/xpcshell/test_langpack.js
new file mode 100644
index 000000000..a97a14d4d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_langpack.js
@@ -0,0 +1,339 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that language packs can be used without restarts.
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// Enable loading extensions from the user scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+// Enable installing distribution add-ons
+Services.prefs.setBoolPref("extensions.installDistroAddons", true);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const userExtDir = gProfD.clone();
+userExtDir.append("extensions2");
+userExtDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userExtDir.parent);
+const distroDir = gProfD.clone();
+distroDir.append("distribution");
+distroDir.append("extensions");
+registerDirectory("XREAppDist", distroDir.parent);
+
+var chrome = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIXULChromeRegistry);
+
+function do_unregister_manifest() {
+ let path = getFileForAddon(profileDir, "langpack-x-testing@tests.mozilla.org");
+ Components.manager.removeBootstrappedManifestLocation(path);
+}
+
+function do_check_locale_not_registered(provider) {
+ let didThrow = false;
+ try {
+ chrome.getSelectedLocale(provider);
+ } catch (e) {
+ didThrow = true;
+ }
+ do_check_true(didThrow);
+}
+
+function run_test() {
+ do_test_pending();
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Tests that installing doesn't require a restart
+function run_test_1() {
+ do_check_locale_not_registered("test-langpack");
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_langpack"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "locale");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Language Pack x-testing");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.hasResource("install.rdf"));
+ do_check_false(install.addon.hasResource("bootstrap.js"));
+ do_check_eq(install.addon.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_INSTALL, 0);
+
+ let addon = install.addon;
+ prepare_test({
+ "langpack-x-testing@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ do_check_true(addon.hasResource("install.rdf"));
+ // spin to let the startup complete
+ do_execute_soon(check_test_1);
+ });
+ install.install();
+ });
+}
+
+function check_test_1() {
+ AddonManager.getAllInstalls(function(installs) {
+ // There should be no active installs now since the install completed and
+ // doesn't require a restart.
+ do_check_eq(installs.length, 0);
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_false(b1.hasResource("bootstrap.js"));
+
+ let dir = do_get_addon_root_uri(profileDir, "langpack-x-testing@tests.mozilla.org");
+
+ AddonManager.getAddonsWithOperationsByTypes(null, function(list) {
+ do_check_eq(list.length, 0);
+
+ run_test_2();
+ });
+ });
+ });
+}
+
+// Tests that disabling doesn't require a restart
+function run_test_2() {
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "langpack-x-testing@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_DISABLE, 0);
+ b1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_true(newb1.userDisabled);
+ do_check_false(newb1.isActive);
+
+ do_execute_soon(run_test_3);
+ });
+ });
+}
+
+// Test that restarting doesn't accidentally re-enable
+function run_test_3() {
+ shutdownManager();
+ startupManager(false);
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_true(b1.userDisabled);
+ do_check_false(b1.isActive);
+
+ run_test_4();
+ });
+}
+
+// Tests that enabling doesn't require a restart
+function run_test_4() {
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "langpack-x-testing@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_ENABLE, 0);
+ b1.userDisabled = false;
+ ensure_test_completed();
+
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(newb1) {
+ do_check_neq(newb1, null);
+ do_check_eq(newb1.version, "1.0");
+ do_check_false(newb1.appDisabled);
+ do_check_false(newb1.userDisabled);
+ do_check_true(newb1.isActive);
+
+ do_execute_soon(run_test_5);
+ });
+ });
+}
+
+// Tests that a restart shuts down and restarts the add-on
+function run_test_5() {
+ shutdownManager();
+ do_unregister_manifest();
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+ startupManager(false);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, b1.id));
+
+ run_test_7();
+ });
+}
+
+// Tests that uninstalling doesn't require a restart
+function run_test_7() {
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "langpack-x-testing@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ do_check_eq(b1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+ b1.uninstall();
+
+ check_test_7();
+ });
+}
+
+function check_test_7() {
+ ensure_test_completed();
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org",
+ callback_soon(function(b1) {
+ do_check_eq(b1, null);
+
+ restartManager();
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(newb1) {
+ do_check_eq(newb1, null);
+
+ do_execute_soon(run_test_8);
+ });
+ }));
+}
+
+// Tests that a locale detected in the profile starts working immediately
+function run_test_8() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_langpack"), profileDir, "langpack-x-testing@tests.mozilla.org");
+
+ startupManager(false);
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org",
+ callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_false(b1.hasResource("bootstrap.js"));
+
+ shutdownManager();
+ do_unregister_manifest();
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+ startupManager(false);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", function(b2) {
+ prepare_test({
+ "langpack-x-testing@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ b2.uninstall();
+ ensure_test_completed();
+ do_execute_soon(run_test_9);
+ });
+ }));
+}
+
+// Tests that a locale from distribution/extensions gets installed and starts
+// working immediately
+function run_test_9() {
+ shutdownManager();
+ manuallyInstall(do_get_addon("test_langpack"), distroDir, "langpack-x-testing@tests.mozilla.org");
+ gAppInfo.version = "2.0";
+ startupManager(true);
+
+ AddonManager.getAddonByID("langpack-x-testing@tests.mozilla.org", callback_soon(function(b1) {
+ do_check_neq(b1, null);
+ do_check_eq(b1.version, "1.0");
+ do_check_false(b1.appDisabled);
+ do_check_false(b1.userDisabled);
+ do_check_true(b1.isActive);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+ do_check_true(b1.hasResource("install.rdf"));
+ do_check_false(b1.hasResource("bootstrap.js"));
+
+ shutdownManager();
+ do_unregister_manifest();
+ // check chrome reg that language pack is not registered
+ do_check_locale_not_registered("test-langpack");
+ startupManager(false);
+ // check chrome reg that language pack is registered
+ do_check_eq(chrome.getSelectedLocale("test-langpack"), "x-testing");
+
+ do_test_finished();
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_locale.js b/toolkit/mozapps/extensions/test/xpcshell/test_locale.js
new file mode 100644
index 000000000..b4c7311e5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locale.js
@@ -0,0 +1,149 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that localized properties work as expected
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+
+function run_test() {
+ do_test_pending();
+
+ // Setup for test
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+
+ startupManager();
+
+ run_test_1();
+}
+
+// Tests that the localized properties are visible before installation
+function run_test_1() {
+ AddonManager.getInstallForFile(do_get_addon("test_locale"), function(install) {
+ do_check_eq(install.addon.name, "fr-FR Name");
+ do_check_eq(install.addon.description, "fr-FR Description");
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(run_test_2));
+ install.install();
+ });
+}
+
+// Tests that the localized properties are visible after installation
+function run_test_2() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+
+ do_check_eq(addon.name, "fr-FR Name");
+ do_check_eq(addon.description, "fr-FR Description");
+
+ addon.userDisabled = true;
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Test that the localized properties are still there when disabled.
+function run_test_3() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "fr-FR Name");
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Localised preference values should be ignored when the add-on is disabled
+function run_test_4() {
+ Services.prefs.setCharPref("extensions.addon1@tests.mozilla.org.name", "Name from prefs");
+ Services.prefs.setCharPref("extensions.addon1@tests.mozilla.org.contributor.1", "Contributor 1");
+ Services.prefs.setCharPref("extensions.addon1@tests.mozilla.org.contributor.2", "Contributor 2");
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ do_check_eq(addon.name, "fr-FR Name");
+ let contributors = addon.contributors;
+ do_check_eq(contributors.length, 3);
+ do_check_eq(contributors[0], "Fr Contributor 1");
+ do_check_eq(contributors[1], "Fr Contributor 2");
+ do_check_eq(contributors[2], "Fr Contributor 3");
+
+ do_execute_soon(run_test_5);
+ });
+}
+
+// Test that changing locale works
+function run_test_5() {
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "de-DE");
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+
+ do_check_eq(addon.name, "de-DE Name");
+ do_check_eq(addon.description, null);
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+// Test that missing locales use the fallbacks
+function run_test_6() {
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "nl-NL");
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(addon) {
+ do_check_neq(addon, null);
+
+ do_check_eq(addon.name, "Fallback Name");
+ do_check_eq(addon.description, "Fallback Description");
+
+ addon.userDisabled = false;
+ do_execute_soon(run_test_7);
+ }));
+}
+
+// Test that the prefs will override the fallbacks
+function run_test_7() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+
+ do_check_eq(addon.name, "Name from prefs");
+
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Test that the prefs will override localized values from the manifest
+function run_test_8() {
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+
+ do_check_eq(addon.name, "Name from prefs");
+ let contributors = addon.contributors;
+ do_check_eq(contributors.length, 2);
+ do_check_eq(contributors[0], "Contributor 1");
+ do_check_eq(contributors[1], "Contributor 2");
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_locked.js b/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
new file mode 100755
index 000000000..86457eab1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked.js
@@ -0,0 +1,544 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we rebuild something sensible from a corrupt database
+
+Components.utils.import("resource://testing-common/httpd.js");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_corrupt.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+// Will be enabled
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will get a compatibility update and stay enabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Will get a compatibility update and be enabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Would stay incompatible with strict compat
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Enabled bootstrapped
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Disabled bootstrapped
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The default theme
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The selected theme
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+add_task(function* init() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ // New profile so new add-ons are ignored
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ let [a2, a3, a4, a7, t2] =
+ yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+ let deferredUpdateFinished = Promise.defer();
+ // Set up the initial state
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ a7.userDisabled = true;
+ t2.userDisabled = false;
+ a3.findUpdates({
+ onUpdateFinished: function() {
+ a4.findUpdates({
+ onUpdateFinished: function() {
+ // Let the updates finish before restarting the manager
+ deferredUpdateFinished.resolve();
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+
+ yield deferredUpdateFinished.promise;
+});
+
+
+add_task(function* run_test_1() {
+ restartManager();
+ let [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // Open another handle on the JSON DB with as much Unix and Windows locking
+ // as we can to simulate some other process interfering with it
+ shutdownManager();
+ do_print("Locking " + gExtensionsJSON.path);
+ let options = {
+ winShare: 0
+ };
+ if (OS.Constants.libc.O_EXLOCK)
+ options.unixFlags = OS.Constants.libc.O_EXLOCK;
+
+ let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
+
+ let filePermissions = gExtensionsJSON.permissions;
+ if (!OS.Constants.Win) {
+ gExtensionsJSON.permissions = 0;
+ }
+ startupManager(false);
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ // Accessing the add-ons should open and recover the database
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ // Should be correctly recovered
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ // Should be correctly recovered
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ // The compatibility update won't be recovered but it should still be
+ // active for this session
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ // The compatibility update won't be recovered and with strict
+ // compatibility it would not have been able to tell that it was
+ // previously userDisabled. However, without strict compat, it wasn't
+ // appDisabled, so it knows it must have been userDisabled.
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ // Should be correctly recovered
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // Restarting will actually apply changes to extensions.ini which will
+ // then be put into the in-memory database when we next fail to load the
+ // real thing
+ try {
+ shutdownManager();
+ } catch (e) {
+ // We're expecting an error here.
+ }
+ startupManager(false);
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // After allowing access to the original DB things should go back to as
+ // they were previously
+ try {
+ shutdownManager();
+ } catch (e) {
+ // We're expecting an error here.
+ }
+ do_print("Unlocking " + gExtensionsJSON.path);
+ yield file.close();
+ gExtensionsJSON.permissions = filePermissions;
+ startupManager();
+
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ try {
+ shutdownManager();
+ } catch (e) {
+ // We're expecting an error here.
+ }
+});
+
+
+function run_test() {
+ run_next_test();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js b/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
new file mode 100644
index 000000000..4a62c585f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked2.js
@@ -0,0 +1,297 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we handle a locked database when there are extension changes
+// in progress
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+// Will be left alone
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be enabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be uninstalled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+
+// Will be updated
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+add_task(function*() {
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+
+ // Make it look like add-on 5 was installed some time in the past so the update is
+ // detected
+ let path = getFileForAddon(profileDir, addon5.id).path;
+ yield promiseSetExtensionModifiedTime(path, Date.now() - (60000));
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+
+ let a1, a2, a3, a4, a5, a6;
+
+ [a2] = yield promiseAddonsByIDs(["addon2@tests.mozilla.org"]);
+ a2.userDisabled = true;
+
+ restartManager();
+
+ [a1, a2, a3, a4, a5] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"]);
+
+ a2.userDisabled = false;
+ a3.userDisabled = true;
+ a4.uninstall();
+
+ yield promiseInstallAllFiles([do_get_addon("test_locked2_5"),
+ do_get_addon("test_locked2_6")]);
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_false(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_ENABLE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_true(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(a4.isActive);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_UNINSTALL);
+ do_check_true(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_eq(a5.version, "1.0");
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_UPGRADE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ // Open another handle on the JSON DB with as much Unix and Windows locking
+ // as we can to simulate some other process interfering with it
+ shutdownManager();
+ do_print("Locking " + gExtensionsJSON.path);
+ let options = {
+ winShare: 0
+ };
+ if (OS.Constants.libc.O_EXLOCK)
+ options.unixFlags = OS.Constants.libc.O_EXLOCK;
+
+ let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
+
+ let filePermissions = gExtensionsJSON.permissions;
+ if (!OS.Constants.Win) {
+ gExtensionsJSON.permissions = 0;
+ }
+ startupManager(false);
+
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+
+ [a1, a2, a3, a4, a5, a6] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_true(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_eq(a4, null);
+
+ do_check_neq(a5, null);
+ do_check_eq(a5.version, "2.0");
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+
+ // After allowing access to the original DB things should still be
+ // back how they were before the lock
+ let shutdownError;
+ try {
+ shutdownManager();
+ } catch (e) {
+ shutdownError = e;
+ }
+ yield file.close();
+ gExtensionsJSON.permissions = filePermissions;
+ startupManager();
+
+ // On Unix, we can save the DB even when the original file wasn't
+ // readable, so our changes were saved. On Windows,
+ // these things happened when we had no access to the database so
+ // they are seen as external changes when we get the database back
+ if (shutdownError) {
+ do_print("Previous XPI save failed");
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED,
+ ["addon6@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED,
+ ["addon4@tests.mozilla.org"]);
+ }
+ else {
+ do_print("Previous XPI save succeeded");
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ }
+
+ [a1, a2, a3, a4, a5, a6] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_true(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_eq(a4, null);
+
+ do_check_neq(a5, null);
+ do_check_eq(a5.version, "2.0");
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a6.id));
+});
+
+function run_test() {
+ run_next_test();
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
new file mode 100644
index 000000000..9e17b4c8b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_locked_strictcompat.js
@@ -0,0 +1,567 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we rebuild something sensible from a corrupt database
+
+Components.utils.import("resource://testing-common/httpd.js");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_corrupt.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+// Will be enabled
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will be disabled
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Will get a compatibility update and be enabled
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Will get a compatibility update and be disabled
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ updateURL: "http://localhost:" + gPort + "/data/test_corrupt.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Stays incompatible
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Enabled bootstrapped
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// Disabled bootstrapped
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ bootstrap: "true",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The default theme
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+// The selected theme
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ internalName: "test/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+add_task(function* init() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Startup the profile and setup the initial state
+ startupManager();
+
+ // New profile so new add-ons are ignored
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ let a1, a2, a3, a4, a5, a6, a7, t1, t2;
+
+ [a2, a3, a4, a7, t2] =
+ yield promiseAddonsByIDs(["addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ // Set up the initial state
+ let deferredUpdateFinished = Promise.defer();
+
+ a2.userDisabled = true;
+ a4.userDisabled = true;
+ a7.userDisabled = true;
+ t2.userDisabled = false;
+ a3.findUpdates({
+ onUpdateFinished: function() {
+ a4.findUpdates({
+ onUpdateFinished: function() {
+ deferredUpdateFinished.resolve();
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ yield deferredUpdateFinished.promise;
+});
+
+add_task(function* run_test_1() {
+ let a1, a2, a3, a4, a5, a6, a7, t1, t2;
+
+ restartManager();
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // Open another handle on the JSON DB with as much Unix and Windows locking
+ // as we can to simulate some other process interfering with it
+ shutdownManager();
+ do_print("Locking " + gExtensionsJSON.path);
+ let options = {
+ winShare: 0
+ };
+ if (OS.Constants.libc.O_EXLOCK)
+ options.unixFlags = OS.Constants.libc.O_EXLOCK;
+
+ let file = yield OS.File.open(gExtensionsJSON.path, {read:true, write:true, existing:true}, options);
+
+ let filePermissions = gExtensionsJSON.permissions;
+ if (!OS.Constants.Win) {
+ gExtensionsJSON.permissions = 0;
+ }
+ startupManager(false);
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ // Accessing the add-ons should open and recover the database
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ // Should be correctly recovered
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ // Should be correctly recovered
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ // The compatibility update won't be recovered but it should still be
+ // active for this session
+ do_check_neq(a3, null);
+ do_check_true(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_DISABLE);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ // The compatibility update won't be recovered and it will not have been
+ // able to tell that it was previously userDisabled
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_false(a4.userDisabled);
+ do_check_true(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ // Should be correctly recovered
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ // Should be correctly recovered
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // Restarting will actually apply changes to extensions.ini which will
+ // then be put into the in-memory database when we next fail to load the
+ // real thing
+ try {
+ shutdownManager();
+ } catch (e) {
+ // We're expecting an error here.
+ }
+ startupManager(false);
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.appDisabled);
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ do_check_false(a4.userDisabled);
+ do_check_true(a4.appDisabled);
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ // After allowing access to the original DB things should go back to as
+ // back how they were before the lock
+ let shutdownError;
+ try {
+ shutdownManager();
+ } catch (e) {
+ shutdownError = e;
+ }
+ do_print("Unlocking " + gExtensionsJSON.path);
+ yield file.close();
+ gExtensionsJSON.permissions = filePermissions;
+ startupManager(false);
+
+ // Shouldn't have seen any startup changes
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+
+ [a1, a2, a3, a4, a5, a6, a7, t1, t2] =
+ yield promiseAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"]);
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_eq(a2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(a3.userDisabled);
+ // On Unix, we may be able to save our changes over the locked DB so we
+ // remember that this extension was changed to disabled. On Windows we
+ // couldn't replace the old DB so we read the older version of the DB
+ // where the extension is enabled
+ if (shutdownError) {
+ do_print("XPI save failed");
+ do_check_true(a3.isActive);
+ do_check_false(a3.appDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ }
+ else {
+ do_print("XPI save succeeded");
+ do_check_false(a3.isActive);
+ do_check_true(a3.appDisabled);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+ }
+ do_check_eq(a3.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a4, null);
+ do_check_false(a4.isActive);
+ // The reverse of the platform difference for a3 - Unix should
+ // stay the same as the last iteration because the save succeeded,
+ // Windows should still say userDisabled
+ if (OS.Constants.Win) {
+ do_check_true(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ }
+ else {
+ do_check_false(a4.userDisabled);
+ do_check_true(a4.appDisabled);
+ }
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+ do_check_eq(a4.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a5, null);
+ do_check_false(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_eq(a5.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_eq(a6.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_true(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_eq(a7.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+
+ do_check_neq(t2, null);
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+
+ try {
+ shutdownManager();
+ } catch (e) {
+ // An error is expected here.
+ }
+});
+
+function run_test() {
+ run_next_test();
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js b/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
new file mode 100644
index 000000000..85279ba5d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_manifest.js
@@ -0,0 +1,562 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This tests that all properties are read from the install manifests and that
+// items are correctly enabled/disabled based on them (blocklist tests are
+// elsewhere)
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ const profileDir = gProfD.clone();
+ profileDir.append("extensions");
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ optionsURL: "chrome://test/content/options.xul",
+ aboutURL: "chrome://test/content/about.xul",
+ iconURL: "chrome://test/skin/icon.png",
+ icon64URL: "chrome://test/skin/icon64.png",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ description: "Test Description",
+ creator: "Test Creator",
+ homepageURL: "http://www.example.com",
+ developer: [
+ "Test Developer 1",
+ "Test Developer 2"
+ ],
+ translator: [
+ "Test Translator 1",
+ "Test Translator 2"
+ ],
+ contributor: [
+ "Test Contributor 1",
+ "Test Contributor 2"
+ ]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "https://www.foo.com",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 2"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://www.foo.com",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 3"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://www.foo.com",
+ updateKey: "foo",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 4"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "*"
+ }],
+ name: "Test Addon 5"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 6"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "0"
+ }],
+ name: "Test Addon 7"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1.1",
+ maxVersion: "*"
+ }],
+ name: "Test Addon 8"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon9@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9.2",
+ maxVersion: "1.9.*"
+ }],
+ name: "Test Addon 9"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon10@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9.2.1",
+ maxVersion: "1.9.*"
+ }],
+ name: "Test Addon 10"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon11@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9",
+ maxVersion: "1.9.2"
+ }],
+ name: "Test Addon 11"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon12@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9",
+ maxVersion: "1.9.1.*"
+ }],
+ name: "Test Addon 12"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon13@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9",
+ maxVersion: "1.9.*"
+ }, {
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "0.5"
+ }],
+ name: "Test Addon 13"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon14@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9",
+ maxVersion: "1.9.1"
+ }, {
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 14"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon15@tests.mozilla.org",
+ version: "1.0",
+ updateKey: "foo",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 15"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon16@tests.mozilla.org",
+ version: "1.0",
+ updateKey: "foo",
+ updateURL: "https://www.foo.com",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 16"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon17@tests.mozilla.org",
+ version: "1.0",
+ optionsURL: "chrome://test/content/options.xul",
+ optionsType: "2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 17"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon18@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 18"
+ }, profileDir, undefined, "options.xul");
+
+ writeInstallRDFForExtension({
+ id: "addon19@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "99",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 19"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon20@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "1",
+ optionsURL: "chrome://test/content/options.xul",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 20"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon21@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "3",
+ optionsURL: "chrome://test/content/options.xul",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 21"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon22@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 22"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon23@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 23"
+ }, profileDir, undefined, "options.xul");
+
+ writeInstallRDFForExtension({
+ id: "addon24@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 24"
+ }, profileDir, undefined, "options.xul");
+
+ writeInstallRDFForExtension({
+ id: "addon25@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 25"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon26@tests.mozilla.org",
+ version: "1.0",
+ optionsType: "4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 26"
+ }, profileDir, undefined, "options.xul");
+
+ do_test_pending();
+ startupManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "addon8@tests.mozilla.org",
+ "addon9@tests.mozilla.org",
+ "addon10@tests.mozilla.org",
+ "addon11@tests.mozilla.org",
+ "addon12@tests.mozilla.org",
+ "addon13@tests.mozilla.org",
+ "addon14@tests.mozilla.org",
+ "addon15@tests.mozilla.org",
+ "addon16@tests.mozilla.org",
+ "addon17@tests.mozilla.org",
+ "addon18@tests.mozilla.org",
+ "addon19@tests.mozilla.org",
+ "addon20@tests.mozilla.org",
+ "addon21@tests.mozilla.org",
+ "addon22@tests.mozilla.org",
+ "addon23@tests.mozilla.org",
+ "addon24@tests.mozilla.org",
+ "addon25@tests.mozilla.org",
+ "addon26@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7, a8, a9, a10,
+ a11, a12, a13, a14, a15, a16, a17, a18, a19, a20,
+ a21, a22, a23, a24, a25, a26]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.type, "extension");
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.optionsURL, "chrome://test/content/options.xul");
+ do_check_eq(a1.optionsType, AddonManager.OPTIONS_TYPE_DIALOG);
+ do_check_eq(a1.aboutURL, "chrome://test/content/about.xul");
+ do_check_eq(a1.iconURL, "chrome://test/skin/icon.png");
+ do_check_eq(a1.icon64URL, "chrome://test/skin/icon64.png");
+ do_check_eq(a1.icons[32], "chrome://test/skin/icon.png");
+ do_check_eq(a1.icons[64], "chrome://test/skin/icon64.png");
+ do_check_eq(a1.name, "Test Addon 1");
+ do_check_eq(a1.description, "Test Description");
+ do_check_eq(a1.creator, "Test Creator");
+ do_check_eq(a1.homepageURL, "http://www.example.com");
+ do_check_eq(a1.developers[0], "Test Developer 1");
+ do_check_eq(a1.developers[1], "Test Developer 2");
+ do_check_eq(a1.translators[0], "Test Translator 1");
+ do_check_eq(a1.translators[1], "Test Translator 2");
+ do_check_eq(a1.contributors[0], "Test Contributor 1");
+ do_check_eq(a1.contributors[1], "Test Contributor 2");
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isCompatible);
+ do_check_true(a1.providesUpdatesSecurely);
+ do_check_eq(a1.blocklistState, AM_Ci.nsIBlocklistService.STATE_NOT_BLOCKED);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_true(a2.isActive);
+ do_check_false(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.providesUpdatesSecurely);
+
+ do_check_neq(a3, null);
+ do_check_eq(a3.id, "addon3@tests.mozilla.org");
+ do_check_false(a3.isActive);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.providesUpdatesSecurely);
+
+ do_check_neq(a4, null);
+ do_check_eq(a4.id, "addon4@tests.mozilla.org");
+ do_check_true(a4.isActive);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.providesUpdatesSecurely);
+
+ do_check_neq(a5, null);
+ do_check_true(a5.isActive);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_true(a5.isCompatible);
+
+ do_check_neq(a6, null);
+ do_check_true(a6.isActive);
+ do_check_false(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_true(a6.isCompatible);
+
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_false(a7.userDisabled);
+ do_check_true(a7.appDisabled);
+ do_check_false(a7.isCompatible);
+
+ do_check_neq(a8, null);
+ do_check_false(a8.isActive);
+ do_check_false(a8.userDisabled);
+ do_check_true(a8.appDisabled);
+ do_check_false(a8.isCompatible);
+
+ do_check_neq(a9, null);
+ do_check_true(a9.isActive);
+ do_check_false(a9.userDisabled);
+ do_check_false(a9.appDisabled);
+ do_check_true(a9.isCompatible);
+
+ do_check_neq(a10, null);
+ do_check_false(a10.isActive);
+ do_check_false(a10.userDisabled);
+ do_check_true(a10.appDisabled);
+ do_check_false(a10.isCompatible);
+
+ do_check_neq(a11, null);
+ do_check_true(a11.isActive);
+ do_check_false(a11.userDisabled);
+ do_check_false(a11.appDisabled);
+ do_check_true(a11.isCompatible);
+
+ do_check_neq(a12, null);
+ do_check_false(a12.isActive);
+ do_check_false(a12.userDisabled);
+ do_check_true(a12.appDisabled);
+ do_check_false(a12.isCompatible);
+
+ do_check_neq(a13, null);
+ do_check_false(a13.isActive);
+ do_check_false(a13.userDisabled);
+ do_check_true(a13.appDisabled);
+ do_check_false(a13.isCompatible);
+
+ do_check_neq(a14, null);
+ do_check_true(a14.isActive);
+ do_check_false(a14.userDisabled);
+ do_check_false(a14.appDisabled);
+ do_check_true(a14.isCompatible);
+
+ do_check_neq(a15, null);
+ do_check_true(a15.isActive);
+ do_check_false(a15.userDisabled);
+ do_check_false(a15.appDisabled);
+ do_check_true(a15.isCompatible);
+ do_check_true(a15.providesUpdatesSecurely);
+
+ do_check_neq(a16, null);
+ do_check_true(a16.isActive);
+ do_check_false(a16.userDisabled);
+ do_check_false(a16.appDisabled);
+ do_check_true(a16.isCompatible);
+ do_check_true(a16.providesUpdatesSecurely);
+
+ do_check_neq(a17, null);
+ do_check_true(a17.isActive);
+ do_check_false(a17.userDisabled);
+ do_check_false(a17.appDisabled);
+ do_check_true(a17.isCompatible);
+ do_check_eq(a17.optionsURL, "chrome://test/content/options.xul");
+ do_check_eq(a17.optionsType, AddonManager.OPTIONS_TYPE_INLINE);
+
+ do_check_neq(a18, null);
+ do_check_true(a18.isActive);
+ do_check_false(a18.userDisabled);
+ do_check_false(a18.appDisabled);
+ do_check_true(a18.isCompatible);
+ if (Services.prefs.getBoolPref("extensions.alwaysUnpack")) {
+ do_check_eq(a18.optionsURL, Services.io.newFileURI(profileDir).spec +
+ "addon18@tests.mozilla.org/options.xul");
+ } else {
+ do_check_eq(a18.optionsURL, "jar:" + Services.io.newFileURI(profileDir).spec +
+ "addon18@tests.mozilla.org.xpi!/options.xul");
+ }
+ do_check_eq(a18.optionsType, AddonManager.OPTIONS_TYPE_INLINE);
+
+ do_check_eq(a19, null);
+
+ do_check_neq(a20, null);
+ do_check_true(a20.isActive);
+ do_check_false(a20.userDisabled);
+ do_check_false(a20.appDisabled);
+ do_check_true(a20.isCompatible);
+ do_check_eq(a20.optionsURL, "chrome://test/content/options.xul");
+ do_check_eq(a20.optionsType, AddonManager.OPTIONS_TYPE_DIALOG);
+
+ do_check_neq(a21, null);
+ do_check_true(a21.isActive);
+ do_check_false(a21.userDisabled);
+ do_check_false(a21.appDisabled);
+ do_check_true(a21.isCompatible);
+ do_check_eq(a21.optionsURL, "chrome://test/content/options.xul");
+ do_check_eq(a21.optionsType, AddonManager.OPTIONS_TYPE_TAB);
+
+ do_check_neq(a22, null);
+ do_check_eq(a22.optionsType, null);
+ do_check_eq(a22.optionsURL, null);
+
+ do_check_neq(a23, null);
+ do_check_eq(a23.optionsType, AddonManager.OPTIONS_TYPE_INLINE);
+ do_check_neq(a23.optionsURL, null);
+
+ do_check_neq(a24, null);
+ do_check_eq(a24.optionsType, AddonManager.OPTIONS_TYPE_INLINE);
+ do_check_neq(a24.optionsURL, null);
+
+ do_check_neq(a25, null);
+ do_check_eq(a25.optionsType, null);
+ do_check_eq(a25.optionsURL, null);
+
+ do_check_neq(a26, null);
+ do_check_eq(a26.optionsType, AddonManager.OPTIONS_TYPE_INLINE_INFO);
+ do_check_neq(a26.optionsURL, null);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js b/toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js
new file mode 100644
index 000000000..1dd05064e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_mapURIToAddonID.js
@@ -0,0 +1,347 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons URIs can be mapped to add-on IDs
+//
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// Enable loading extensions from the user scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const userExtDir = gProfD.clone();
+userExtDir.append("extensions2");
+userExtDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userExtDir.parent);
+
+BootstrapMonitor.init();
+
+function TestProvider(result) {
+ this.result = result;
+}
+TestProvider.prototype = {
+ uri: Services.io.newURI("hellow://world", null, null),
+ id: "valid@id",
+ startup: function() {},
+ shutdown: function() {},
+ mapURIToAddonID: function(aURI) {
+ if (aURI.spec === this.uri.spec) {
+ return this.id;
+ }
+ throw Components.Exception("Not mapped", this.result);
+ }
+};
+
+function TestProviderNoMap() {}
+TestProviderNoMap.prototype = {
+ startup: function() {},
+ shutdown: function() {}
+};
+
+function check_mapping(uri, id) {
+ do_check_eq(AddonManager.mapURIToAddonID(uri), id);
+ let svc = Components.classes["@mozilla.org/addons/integration;1"].
+ getService(Components.interfaces.amIAddonManager);
+ let val = {};
+ do_check_true(svc.mapURIToAddonID(uri, val));
+ do_check_eq(val.value, id);
+}
+
+function getActiveVersion() {
+ return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+ do_test_pending();
+
+ run_test_early();
+}
+
+function run_test_early() {
+ startupManager();
+
+ installAllFiles([do_get_addon("test_chromemanifest_1")], function() {
+ restartManager();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(addon) {
+ let uri = addon.getResourceURI(".");
+ let id = addon.id;
+ check_mapping(uri, id);
+
+ shutdownManager();
+
+ // Force an early call, to check that mappings will get correctly
+ // initialized when the manager actually starts up.
+ // See bug 957089
+
+ // First force-initialize the XPIProvider.
+ let s = Components.utils.import(
+ "resource://gre/modules/addons/XPIProvider.jsm", {});
+
+ // Make the early API call.
+ // AddonManager still misses its provider and so doesn't work yet.
+ do_check_null(AddonManager.mapURIToAddonID(uri));
+ // But calling XPIProvider directly works immediately
+ do_check_eq(s.XPIProvider.mapURIToAddonID(uri), id);
+
+ // Actually start up the manager.
+ startupManager(false);
+
+ // Check that the mapping is there now.
+ check_mapping(uri, id);
+ do_check_eq(s.XPIProvider.mapURIToAddonID(uri), id);
+
+ run_test_nomapping();
+ });
+ });
+}
+
+function run_test_nomapping() {
+ do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
+ try {
+ let svc = Components.classes["@mozilla.org/addons/integration;1"].
+ getService(Components.interfaces.amIAddonManager);
+ let val = {};
+ do_check_false(svc.mapURIToAddonID(TestProvider.prototype.uri, val));
+ }
+ catch (ex) {
+ do_throw(ex);
+ }
+
+ run_test_1();
+}
+
+
+// Tests that add-on URIs are mappable after an install
+function run_test_1() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), function(install) {
+ ensure_test_completed();
+
+ let addon = install.addon;
+ prepare_test({
+ "bootstrap1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], function() {
+ let uri = addon.getResourceURI(".");
+ check_mapping(uri, addon.id);
+
+ BootstrapMonitor.promiseAddonStartup("bootstrap1@tests.mozilla.org").then(function() {
+ run_test_2(uri);
+ });
+ });
+ install.install();
+ });
+}
+
+// Tests that add-on URIs are still mappable, even after the add-on gets
+// disabled in-session.
+function run_test_2(uri) {
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "bootstrap1@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ b1.userDisabled = true;
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
+ do_check_true(newb1.userDisabled);
+ check_mapping(uri, newb1.id);
+
+ do_execute_soon(() => run_test_3(uri));
+ });
+ });
+}
+
+// Tests that add-on URIs are mappable if the add-on was never started in a
+// session
+function run_test_3(uri) {
+ restartManager();
+
+ check_mapping(uri, "bootstrap1@tests.mozilla.org");
+
+ run_test_4();
+}
+
+// Tests that add-on URIs are mappable after a restart + reenable
+function run_test_4() {
+ restartManager();
+
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "bootstrap1@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ b1.userDisabled = false;
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(newb1) {
+ let uri = newb1.getResourceURI(".");
+ check_mapping(uri, newb1.id);
+
+ do_execute_soon(run_test_5);
+ });
+ });
+}
+
+// Tests that add-on URIs are mappable after a restart
+function run_test_5() {
+ restartManager();
+
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+ let uri = b1.getResourceURI(".");
+ check_mapping(uri, b1.id);
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+// Tests that add-on URIs are mappable after being uninstalled
+function run_test_6() {
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+ prepare_test({
+ "bootstrap1@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ let uri = b1.getResourceURI(".");
+ b1.uninstall();
+ ensure_test_completed();
+
+ check_mapping(uri, b1.id);
+
+ restartManager();
+ do_execute_soon(run_test_7);
+ });
+}
+
+// Tests that add-on URIs are mappable for add-ons detected at startup
+function run_test_7() {
+ shutdownManager();
+
+ manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, "bootstrap1@tests.mozilla.org");
+
+ startupManager();
+
+ AddonManager.getAddonByID("bootstrap1@tests.mozilla.org", function(b1) {
+ let uri = b1.getResourceURI(".");
+ check_mapping(uri, b1.id);
+
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Tests that temporary addon-on URIs are mappable after install and uninstall
+function run_test_8() {
+ prepare_test({
+ "bootstrap2@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onExternalInstall",
+ ], function(b2) {
+ let uri = b2.getResourceURI(".");
+ check_mapping(uri, b2.id);
+
+ prepare_test({
+ "bootstrap2@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ b2.uninstall();
+ ensure_test_completed();
+
+ check_mapping(uri, b2.id);
+
+ do_execute_soon(run_test_invalidarg);
+ });
+ AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap2_1"));
+}
+
+// Tests that the AddonManager will bail when mapURIToAddonID is called with an
+// invalid argument
+function run_test_invalidarg() {
+ restartManager();
+
+ let tests = [undefined,
+ null,
+ 1,
+ "string",
+ "chrome://global/content/",
+ function() {}
+ ];
+ for (var test of tests) {
+ try {
+ AddonManager.mapURIToAddonID(test);
+ throw new Error("Shouldn't be able to map the URI in question");
+ }
+ catch (ex) {
+ if (ex.result) {
+ do_check_eq(ex.result, Components.results.NS_ERROR_INVALID_ARG);
+ } else {
+ do_throw(ex);
+ }
+ }
+ }
+
+ run_test_provider();
+}
+
+// Tests that custom providers are correctly handled
+function run_test_provider() {
+ restartManager();
+
+ const provider = new TestProvider(Components.results.NS_ERROR_NOT_AVAILABLE);
+ AddonManagerPrivate.registerProvider(provider);
+
+ check_mapping(provider.uri, provider.id);
+
+ let u2 = provider.uri.clone();
+ u2.path = "notmapped";
+ do_check_eq(AddonManager.mapURIToAddonID(u2), null);
+
+ AddonManagerPrivate.unregisterProvider(provider);
+
+ run_test_provider_nomap();
+}
+
+// Tests that custom providers are correctly handled, even not implementing
+// mapURIToAddonID
+function run_test_provider_nomap() {
+ restartManager();
+
+ const provider = new TestProviderNoMap();
+ AddonManagerPrivate.registerProvider(provider);
+
+ do_check_eq(AddonManager.mapURIToAddonID(TestProvider.prototype.uri), null);
+
+ AddonManagerPrivate.unregisterProvider(provider);
+
+ do_test_finished();
+}
+
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_metadata_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_metadata_update.js
new file mode 100644
index 000000000..dbf5db485
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_metadata_update.js
@@ -0,0 +1,159 @@
+/* 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/.
+ */
+
+/*
+ * Test whether we need to block start-up to check add-on compatibility,
+ * based on how long since the last succesful metadata ping.
+ * All tests depend on having one add-on installed in the profile
+ */
+
+
+const URI_EXTENSION_UPDATE_DIALOG = "chrome://mozapps/content/extensions/update.xul";
+const PREF_EM_SHOW_MISMATCH_UI = "extensions.showMismatchUI";
+
+// Constants copied from AddonRepository.jsm
+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
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+// None of this works without the add-on repository cache
+Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+var Cr = Components.results;
+
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+var testserver;
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// This will be called to show the compatibility update dialog.
+var WindowWatcher = {
+ expected: false,
+
+ openWindow: function(parent, url, name, features, args) {
+ do_check_true(Services.startup.interrupted);
+ do_check_eq(url, URI_EXTENSION_UPDATE_DIALOG);
+ do_check_true(this.expected);
+ this.expected = false;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+// Return Date.now() in seconds, rounded
+function now() {
+ return Math.round(Date.now() / 1000);
+}
+
+// First time with a new profile, so we don't have a cache.lastUpdate pref
+add_task(function* checkFirstMetadata() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ Services.prefs.setBoolPref(PREF_EM_SHOW_MISMATCH_UI, true);
+
+ // Create and configure the HTTP server.
+ testserver = createHttpServer();
+ testserver.registerDirectory("/data/", do_get_file("data"));
+ testserver.registerDirectory("/addons/", do_get_file("addons"));
+ gPort = testserver.identity.primaryPort;
+ const BASE_URL = "http://localhost:" + gPort;
+ const GETADDONS_RESULTS = BASE_URL + "/data/test_AddonRepository_cache.xml";
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, GETADDONS_RESULTS);
+
+ // Put an add-on in our profile so the metadata check will have something to do
+ var min1max2 = {
+ id: "min1max2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test addon compatible with v1->v2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ };
+ writeInstallRDFForExtension(min1max2, profileDir);
+
+ startupManager();
+
+ // Make sure that updating metadata for the first time sets the lastUpdate preference
+ yield AddonRepository.repopulateCache();
+ do_print("Update done, getting last update");
+ let lastUpdate = Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE);
+ do_check_true(lastUpdate > 0);
+
+ // Make sure updating metadata again updates the preference
+ let oldUpdate = lastUpdate - 2 * DEFAULT_METADATA_UPDATETHRESHOLD_SEC;
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, oldUpdate);
+ yield AddonRepository.repopulateCache();
+ do_check_neq(oldUpdate, Services.prefs.getIntPref(PREF_METADATA_LASTUPDATE));
+});
+
+// First upgrade with no lastUpdate pref and no add-ons changing shows UI
+add_task(function* upgrade_no_lastupdate() {
+ Services.prefs.clearUserPref(PREF_METADATA_LASTUPDATE);
+
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("2");
+ do_check_false(WindowWatcher.expected);
+});
+
+// Upgrade with lastUpdate more than default threshold and no add-ons changing shows UI
+add_task(function* upgrade_old_lastupdate() {
+ let oldEnough = now() - DEFAULT_METADATA_UPDATETHRESHOLD_SEC - 1000;
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, oldEnough);
+
+ WindowWatcher.expected = true;
+ // upgrade, downgrade, it has the same effect on the code path under test
+ yield promiseRestartManager("1");
+ do_check_false(WindowWatcher.expected);
+});
+
+// Upgrade with lastUpdate less than default threshold and no add-ons changing doesn't show
+add_task(function* upgrade_young_lastupdate() {
+ let notOldEnough = now() - DEFAULT_METADATA_UPDATETHRESHOLD_SEC + 1000;
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, notOldEnough);
+
+ WindowWatcher.expected = false;
+ yield promiseRestartManager("2");
+ do_check_false(WindowWatcher.expected);
+});
+
+// Repeat more-than and less-than but with updateThreshold preference set
+// Upgrade with lastUpdate more than pref threshold and no add-ons changing shows UI
+const TEST_UPDATETHRESHOLD_SEC = 50000;
+add_task(function* upgrade_old_pref_lastupdate() {
+ Services.prefs.setIntPref(PREF_METADATA_UPDATETHRESHOLD_SEC, TEST_UPDATETHRESHOLD_SEC);
+
+ let oldEnough = now() - TEST_UPDATETHRESHOLD_SEC - 1000;
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, oldEnough);
+
+ WindowWatcher.expected = true;
+ yield promiseRestartManager("1");
+ do_check_false(WindowWatcher.expected);
+});
+
+// Upgrade with lastUpdate less than pref threshold and no add-ons changing doesn't show
+add_task(function* upgrade_young_pref_lastupdate() {
+ let notOldEnough = now() - TEST_UPDATETHRESHOLD_SEC + 1000;
+ Services.prefs.setIntPref(PREF_METADATA_LASTUPDATE, notOldEnough);
+
+ WindowWatcher.expected = false;
+ yield promiseRestartManager("2");
+ do_check_false(WindowWatcher.expected);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
new file mode 100644
index 000000000..8c13593b9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate1.js
@@ -0,0 +1,231 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we migrate data from the old rdf style database
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ type: 4,
+ internalName: "theme1/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ type: 4,
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ let stagedXPIs = profileDir.clone();
+ stagedXPIs.append("staged-xpis");
+ stagedXPIs.append("addon6@tests.mozilla.org");
+ stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let addon6 = do_get_addon("test_migrate6");
+ addon6.copyTo(stagedXPIs, "tmp.xpi");
+ stagedXPIs = stagedXPIs.parent;
+
+ stagedXPIs.append("addon7@tests.mozilla.org");
+ stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ let addon7 = do_get_addon("test_migrate7");
+ addon7.copyTo(stagedXPIs, "tmp.xpi");
+ stagedXPIs = stagedXPIs.parent;
+
+ stagedXPIs.append("addon8@tests.mozilla.org");
+ stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ let addon8 = do_get_addon("test_migrate8");
+ addon8.copyTo(stagedXPIs, "tmp.xpi");
+ stagedXPIs = stagedXPIs.parent;
+
+ let old = do_get_file("data/test_migrate.rdf");
+ old.copyTo(gProfD, "extensions.rdf");
+
+ let oldCache = gProfD.clone();
+ oldCache.append("extensions.cache");
+ oldCache.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+
+ // Theme state is determined by the selected theme pref
+ Services.prefs.setCharPref("general.skins.selectedSkin", "theme1/1.0");
+
+ Services.prefs.setCharPref("extensions.lastAppVersion", "1");
+
+ startupManager();
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ do_check_false(oldCache.exists());
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "addon8@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a1, a2, a3,
+ a4, a5, a6,
+ a7, a8, t1,
+ t2]) {
+ // addon1 was user and app enabled in the old extensions.rdf
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_false(a1.hasBinaryComponents);
+ do_check_true(a1.seen);
+
+ // addon2 was user disabled and app enabled in the old extensions.rdf
+ do_check_neq(a2, null);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(a2.hasBinaryComponents);
+ do_check_true(a2.seen);
+
+ // addon3 was pending user disable and app disabled in the old extensions.rdf
+ do_check_neq(a3, null);
+ do_check_true(a3.userDisabled);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_false(a3.hasBinaryComponents);
+ do_check_true(a3.seen);
+
+ // addon4 was pending user enable and app disabled in the old extensions.rdf
+ do_check_neq(a4, null);
+ do_check_false(a4.userDisabled);
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+ do_check_false(a4.hasBinaryComponents);
+ do_check_true(a4.seen);
+
+ // addon5 was disabled and compatible but a new version has been installed
+ // since, it should still be disabled but should be incompatible
+ do_check_neq(a5, null);
+ do_check_true(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_false(a5.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+ do_check_false(a5.hasBinaryComponents);
+ do_check_true(a5.seen);
+
+ // addon6, addon7 and addon8 will have been lost as they were staged in the
+ // pre-Firefox 4.0 directory
+ do_check_eq(a6, null);
+ do_check_eq(a7, null);
+ do_check_eq(a8, null);
+
+ // Theme 1 was previously enabled
+ do_check_neq(t1, null);
+ do_check_false(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_true(t1.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_true(t1.seen);
+
+ // Theme 2 was previously disabled
+ do_check_neq(t2, null);
+ do_check_true(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_false(t2.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, t2.id));
+ do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_true(t2.seen);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js
new file mode 100644
index 000000000..cc7336713
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate2.js
@@ -0,0 +1,267 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we migrate data from SQLITE databases
+// Note that since the database doesn't contain the foreignInstall field we
+// should just assume that no add-ons in the user profile were foreignInstalls
+
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +
+ AddonManager.SCOPE_SYSTEM);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 4",
+ strictCompatibility: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "0"
+ }]
+};
+
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 6",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "0"
+ }]
+};
+
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 7",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon8 = {
+ id: "addon8@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 8",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const globalDir = gProfD.clone();
+globalDir.append("extensions2");
+globalDir.append(gAppInfo.ID);
+registerDirectory("XRESysSExtPD", globalDir.parent);
+const userDir = gProfD.clone();
+userDir.append("extensions3");
+userDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userDir.parent);
+
+function run_test() {
+ do_test_pending();
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, globalDir);
+ writeInstallRDFForExtension(addon8, userDir);
+
+ // Write out a minimal database
+ let dbfile = gProfD.clone();
+ dbfile.append("extensions.sqlite");
+ let db = AM_Cc["@mozilla.org/storage/service;1"].
+ getService(AM_Ci.mozIStorageService).
+ openDatabase(dbfile);
+ db.createTable("addon", "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "id TEXT, location TEXT, version TEXT, active INTEGER, " +
+ "userDisabled INTEGER, installDate INTEGER");
+ db.createTable("targetApplication", "addon_internal_id INTEGER, " +
+ "id TEXT, minVersion TEXT, maxVersion TEXT");
+ let stmt = db.createStatement("INSERT INTO addon VALUES (NULL, :id, :location, " +
+ ":version, :active, :userDisabled, :installDate)");
+
+ let internal_ids = {};
+
+ [["addon1@tests.mozilla.org", "app-profile", "1.0", "1", "0", "0"],
+ ["addon2@tests.mozilla.org", "app-profile", "2.0", "0", "1", "0"],
+ ["addon3@tests.mozilla.org", "app-profile", "2.0", "1", "1", "0"],
+ ["addon4@tests.mozilla.org", "app-profile", "2.0", "0", "0", "0"],
+ ["addon5@tests.mozilla.org", "app-profile", "2.0", "1", "0", "0"],
+ ["addon6@tests.mozilla.org", "app-profile", "1.0", "0", "1", "0"],
+ ["addon7@tests.mozilla.org", "app-system-share", "1.0", "1", "0", "0"],
+ ["addon8@tests.mozilla.org", "app-system-user", "1.0", "1", "0", "0"]].forEach(function(a) {
+ stmt.params.id = a[0];
+ stmt.params.location = a[1];
+ stmt.params.version = a[2];
+ stmt.params.active = a[3];
+ stmt.params.userDisabled = a[4];
+ stmt.params.installDate = a[5];
+ stmt.execute();
+ internal_ids[a[0]] = db.lastInsertRowID;
+ });
+ stmt.finalize();
+
+ // Add updated target application into for addon5
+ stmt = db.createStatement("INSERT INTO targetApplication VALUES " +
+ "(:internal_id, :id, :minVersion, :maxVersion)");
+ stmt.params.internal_id = internal_ids["addon5@tests.mozilla.org"];
+ stmt.params.id = "xpcshell@tests.mozilla.org";
+ stmt.params.minVersion = "0";
+ stmt.params.maxVersion = "1";
+ stmt.execute();
+
+ // Add updated target application into for addon6
+ stmt.params.internal_id = internal_ids["addon6@tests.mozilla.org"];
+ stmt.params.id = "xpcshell@tests.mozilla.org";
+ stmt.params.minVersion = "0";
+ stmt.params.maxVersion = "1";
+ stmt.execute();
+ stmt.finalize();
+
+ db.schemaVersion = 10000;
+ Services.prefs.setIntPref("extensions.databaseSchema", 14);
+ db.close();
+
+ startupManager();
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "addon8@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7, a8]) {
+ // addon1 was enabled in the database
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_false(a1.strictCompatibility);
+ do_check_false(a1.foreignInstall);
+ do_check_true(a1.seen);
+ // addon2 was disabled in the database
+ do_check_neq(a2, null);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(a2.strictCompatibility);
+ do_check_false(a2.foreignInstall);
+ do_check_true(a2.seen);
+ // addon3 was pending-disable in the database
+ do_check_neq(a3, null);
+ do_check_true(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(a3.strictCompatibility);
+ do_check_false(a3.foreignInstall);
+ do_check_true(a3.seen);
+ // addon4 was pending-enable in the database
+ do_check_neq(a4, null);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.isActive);
+ do_check_true(a4.strictCompatibility);
+ do_check_false(a4.foreignInstall);
+ do_check_true(a4.seen);
+ // addon5 was enabled in the database but needed a compatibility update
+ do_check_neq(a5, null);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_true(a5.isActive);
+ do_check_false(a5.strictCompatibility);
+ do_check_false(a5.foreignInstall);
+ do_check_true(a5.seen);
+ // addon6 was disabled and compatible but a new version has been installed
+ // since, it should still be disabled but should be incompatible
+ do_check_neq(a6, null);
+ do_check_true(a6.userDisabled);
+ do_check_true(a6.appDisabled);
+ do_check_false(a6.isActive);
+ do_check_false(a6.strictCompatibility);
+ do_check_false(a6.foreignInstall);
+ do_check_true(a6.seen);
+ // addon7 is in the global install location so should be a foreignInstall
+ do_check_neq(a7, null);
+ do_check_false(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_true(a7.isActive);
+ do_check_false(a7.strictCompatibility);
+ do_check_true(a7.foreignInstall);
+ do_check_true(a7.seen);
+ // addon8 is in the user install location so should be a foreignInstall
+ do_check_neq(a8, null);
+ do_check_false(a8.userDisabled);
+ do_check_false(a8.appDisabled);
+ do_check_true(a8.isActive);
+ do_check_false(a8.strictCompatibility);
+ do_check_true(a8.foreignInstall);
+ do_check_true(a8.seen);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
new file mode 100644
index 000000000..71bd66603
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate3.js
@@ -0,0 +1,229 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we migrate data from the old extensions.rdf database. This
+// matches test_migrate1.js however it runs with a lightweight theme selected
+// so the themes should appear disabled.
+
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 1",
+ type: 4,
+ internalName: "theme1/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var theme2 = {
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Theme 2",
+ type: 4,
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+ writeInstallRDFForExtension(theme2, profileDir);
+
+ // Cannot use the LightweightThemeManager before AddonManager has been started
+ // so inject the correct prefs
+ Services.prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify([{
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost/data/index.html",
+ headerURL: "http://localhost/data/header.png",
+ footerURL: "http://localhost/data/footer.png",
+ previewURL: "http://localhost/data/preview.png",
+ iconURL: "http://localhost/data/icon.png"
+ }]));
+ Services.prefs.setCharPref("lightweightThemes.selectedThemeID", "1");
+
+ let stagedXPIs = profileDir.clone();
+ stagedXPIs.append("staged-xpis");
+ stagedXPIs.append("addon6@tests.mozilla.org");
+ stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let addon6 = do_get_addon("test_migrate6");
+ addon6.copyTo(stagedXPIs, "tmp.xpi");
+ stagedXPIs = stagedXPIs.parent;
+
+ stagedXPIs.append("addon7@tests.mozilla.org");
+ stagedXPIs.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ let addon7 = do_get_addon("test_migrate7");
+ addon7.copyTo(stagedXPIs, "tmp.xpi");
+ stagedXPIs = stagedXPIs.parent;
+
+ let old = do_get_file("data/test_migrate.rdf");
+ old.copyTo(gProfD, "extensions.rdf");
+
+ // Theme state is determined by the selected theme pref
+ Services.prefs.setCharPref("general.skins.selectedSkin", "theme1/1.0");
+
+ startupManager();
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([a1, a2, a3,
+ a4, a5, a6,
+ a7, t1, t2]) {
+ // addon1 was user and app enabled in the old extensions.rdf
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(a1.seen);
+
+ // addon2 was user disabled and app enabled in the old extensions.rdf
+ do_check_neq(a2, null);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(a2.seen);
+
+ // addon3 was pending user disable and app disabled in the old extensions.rdf
+ do_check_neq(a3, null);
+ do_check_true(a3.userDisabled);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(a3.seen);
+
+ // addon4 was pending user enable and app disabled in the old extensions.rdf
+ do_check_neq(a4, null);
+ do_check_false(a4.userDisabled);
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+ do_check_true(a4.seen);
+
+ // addon5 was disabled and compatible but a new version has been installed
+ // since, it should still be disabled but should be incompatible
+ do_check_neq(a5, null);
+ do_check_true(a5.userDisabled);
+ do_check_true(a5.appDisabled);
+ do_check_false(a5.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+ do_check_true(a5.seen);
+
+ // addon6 and addon7 will have been lost as they were staged in the
+ // pre-Firefox 4.0 directory
+ do_check_eq(a6, null);
+ do_check_eq(a7, null);
+
+ // Theme 1 was previously disabled
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_false(t1.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_true(t1.seen);
+
+ // Theme 2 was previously disabled
+ do_check_neq(t2, null);
+ do_check_true(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_false(t2.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, t2.id));
+ do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_true(t2.seen);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js
new file mode 100644
index 000000000..fad015886
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate4.js
@@ -0,0 +1,321 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we migrate data from a previous version of the JSON database
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_migrate4.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 4",
+ strictCompatibility: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 5",
+ updateURL: "http://localhost:" + gPort + "/data/test_migrate4.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "1"
+ }]
+};
+
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ updateURL: "http://localhost:" + gPort + "/data/test_migrate4.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "1"
+ }]
+};
+
+var defaultTheme = {
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Default",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var oldSyncGUIDs = {};
+
+function prepare_profile() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(defaultTheme, profileDir);
+
+ startupManager();
+ installAllFiles([do_get_addon("test_migrate8"), do_get_addon("test_migrate9")],
+ function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon9@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a9]) {
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+ a2.userDisabled = true;
+ a2.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ a3.applyBackgroundUpdates = AddonManager.AUTOUPDATE_ENABLE;
+ a4.userDisabled = true;
+ a6.userDisabled = true;
+ a9.userDisabled = false;
+
+ for (let addon of [a1, a2, a3, a4, a5, a6]) {
+ oldSyncGUIDs[addon.id] = addon.syncGUID;
+ }
+
+ a6.findUpdates({
+ onUpdateAvailable: function(aAddon, aInstall6) {
+ AddonManager.getInstallForURL("http://localhost:" + gPort + "/addons/test_migrate4_7.xpi", function(aInstall7) {
+ completeAllInstalls([aInstall6, aInstall7], function() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org"],
+ function([a1_2, a2_2, a3_2, a4_2, a5_2, a6_2]) {
+ a3_2.userDisabled = true;
+ a4_2.userDisabled = false;
+
+ a5_2.findUpdates({
+ onUpdateFinished: function() {
+ do_execute_soon(perform_migration);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+ }, "application/x-xpinstall");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+}
+
+function perform_migration() {
+ shutdownManager();
+
+ // Turn on disabling for all scopes
+ Services.prefs.setIntPref("extensions.autoDisableScopes", 15);
+
+ changeXPIDBVersion(1, data => {
+ // Delete the seen property from all add-ons to make sure it defaults to true
+ for (let addon of data.addons) {
+ delete addon.seen;
+ }
+ });
+ Services.prefs.setIntPref("extensions.databaseSchema", 1);
+
+ gAppInfo.version = "2"
+ startupManager(true);
+ test_results();
+}
+
+function test_results() {
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org",
+ "addon8@tests.mozilla.org",
+ "addon9@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7, a8, a9]) {
+ // addon1 was enabled
+ do_check_neq(a1, null);
+ do_check_eq(a1.syncGUID, oldSyncGUIDs[a1.id]);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_true(a1.foreignInstall);
+ do_check_true(a1.seen);
+ do_check_false(a1.hasBinaryComponents);
+ do_check_false(a1.strictCompatibility);
+
+ // addon2 was disabled
+ do_check_neq(a2, null);
+ do_check_eq(a2.syncGUID, oldSyncGUIDs[a2.id]);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ do_check_eq(a2.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
+ do_check_true(a2.foreignInstall);
+ do_check_true(a2.seen);
+ do_check_false(a2.hasBinaryComponents);
+ do_check_false(a2.strictCompatibility);
+
+ // addon3 was pending-disable in the database
+ do_check_neq(a3, null);
+ do_check_eq(a3.syncGUID, oldSyncGUIDs[a3.id]);
+ do_check_true(a3.userDisabled);
+ do_check_false(a3.appDisabled);
+ do_check_false(a3.isActive);
+ do_check_eq(a3.applyBackgroundUpdates, AddonManager.AUTOUPDATE_ENABLE);
+ do_check_true(a3.foreignInstall);
+ do_check_true(a3.seen);
+ do_check_false(a3.hasBinaryComponents);
+ do_check_false(a3.strictCompatibility);
+
+ // addon4 was pending-enable in the database
+ do_check_neq(a4, null);
+ do_check_eq(a4.syncGUID, oldSyncGUIDs[a4.id]);
+ do_check_false(a4.userDisabled);
+ do_check_false(a4.appDisabled);
+ do_check_true(a4.isActive);
+ do_check_eq(a4.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_true(a4.foreignInstall);
+ do_check_true(a4.seen);
+ do_check_false(a4.hasBinaryComponents);
+ do_check_true(a4.strictCompatibility);
+
+ // addon5 was enabled in the database but needed a compatibility update
+ do_check_neq(a5, null);
+ do_check_false(a5.userDisabled);
+ do_check_false(a5.appDisabled);
+ do_check_true(a5.isActive);
+ do_check_eq(a4.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_true(a5.foreignInstall);
+ do_check_true(a5.seen);
+ do_check_false(a5.hasBinaryComponents);
+ do_check_false(a5.strictCompatibility);
+
+ // addon6 was disabled and compatible but a new version has been installed
+ do_check_neq(a6, null);
+ do_check_eq(a6.syncGUID, oldSyncGUIDs[a6.id]);
+ do_check_eq(a6.version, "2.0");
+ do_check_true(a6.userDisabled);
+ do_check_false(a6.appDisabled);
+ do_check_false(a6.isActive);
+ do_check_eq(a6.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_true(a6.foreignInstall);
+ do_check_true(a6.seen);
+ do_check_eq(a6.sourceURI.spec, "http://localhost:" + gPort + "/addons/test_migrate4_6.xpi");
+ do_check_eq(a6.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+ do_check_false(a6.hasBinaryComponents);
+ do_check_false(a6.strictCompatibility);
+
+ // addon7 was installed manually
+ do_check_neq(a7, null);
+ do_check_eq(a7.version, "1.0");
+ do_check_false(a7.userDisabled);
+ do_check_false(a7.appDisabled);
+ do_check_true(a7.isActive);
+ do_check_eq(a7.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_false(a7.foreignInstall);
+ do_check_true(a7.seen);
+ do_check_eq(a7.sourceURI.spec, "http://localhost:" + gPort + "/addons/test_migrate4_7.xpi");
+ do_check_eq(a7.releaseNotesURI, null);
+ do_check_false(a7.hasBinaryComponents);
+ do_check_false(a7.strictCompatibility);
+
+ // addon8 was enabled and has binary components
+ do_check_neq(a8, null);
+ do_check_false(a8.userDisabled);
+ do_check_false(a8.appDisabled);
+ do_check_true(a8.isActive);
+ do_check_false(a8.foreignInstall);
+ do_check_true(a8.seen);
+ do_check_true(a8.hasBinaryComponents);
+ do_check_false(a8.strictCompatibility);
+
+ // addon9 is the active theme
+ do_check_neq(a9, null);
+ do_check_false(a9.userDisabled);
+ do_check_false(a9.appDisabled);
+ do_check_true(a9.isActive);
+ do_check_false(a9.foreignInstall);
+ do_check_true(a9.seen);
+ do_check_false(a9.hasBinaryComponents);
+ do_check_true(a9.strictCompatibility);
+
+ testserver.stop(do_test_finished);
+ });
+}
+
+function run_test() {
+ do_test_pending();
+
+ prepare_profile();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate5.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate5.js
new file mode 100644
index 000000000..885cd5a7c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate5.js
@@ -0,0 +1,139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we fail to migrate but still start up ok when there is a SQLITE database
+// with no useful data in it.
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0",
+ maxVersion: "0"
+ }]
+};
+
+var defaultTheme = {
+ id: "default@tests.mozilla.org",
+ version: "2.0",
+ name: "Default theme",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "2.0",
+ name: "Test theme",
+ internalName: "theme1/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(defaultTheme, profileDir);
+ writeInstallRDFForExtension(theme1, profileDir);
+
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, "theme1/1.0");
+
+ // Write out a broken database (no userDisabled field)
+ let dbfile = gProfD.clone();
+ dbfile.append("extensions.sqlite");
+ let db = AM_Cc["@mozilla.org/storage/service;1"].
+ getService(AM_Ci.mozIStorageService).
+ openDatabase(dbfile);
+ db.createTable("addon", "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "id TEXT, location TEXT, version TEXT, active INTEGER, " +
+ "installDate INTEGER");
+ db.createTable("targetApplication", "addon_internal_id INTEGER, " +
+ "id TEXT, minVersion TEXT, maxVersion TEXT");
+ let stmt = db.createStatement("INSERT INTO addon VALUES (NULL, :id, :location, " +
+ ":version, :active, :installDate)");
+
+ let internal_ids = {};
+
+ [["addon1@tests.mozilla.org", "app-profile", "1.0", "1", "0"],
+ ["addon2@tests.mozilla.org", "app-profile", "2.0", "0", "0"],
+ ["default@tests.mozilla.org", "app-profile", "2.0", "1", "0"],
+ ["theme1@tests.mozilla.org", "app-profile", "2.0", "0", "0"]].forEach(function(a) {
+ stmt.params.id = a[0];
+ stmt.params.location = a[1];
+ stmt.params.version = a[2];
+ stmt.params.active = a[3];
+ stmt.params.installDate = a[4];
+ stmt.execute();
+ internal_ids[a[0]] = db.lastInsertRowID;
+ });
+ stmt.finalize();
+
+ db.schemaVersion = 100;
+ Services.prefs.setIntPref("extensions.databaseSchema", 100);
+ db.close();
+
+ startupManager();
+ check_startup_changes("installed", []);
+ check_startup_changes("updated", []);
+ check_startup_changes("uninstalled", []);
+ check_startup_changes("disabled", []);
+ check_startup_changes("enabled", []);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"],
+ function([a1, a2, d, t1]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.userDisabled);
+ do_check_true(a2.appDisabled);
+ do_check_false(a2.isActive);
+
+ // Should have enabled the selected theme
+ do_check_neq(t1, null);
+ do_check_false(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_true(t1.isActive);
+
+ do_check_neq(d, null);
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_false(d.isActive);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrateAddonRepository.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrateAddonRepository.js
new file mode 100644
index 000000000..d9cfc8790
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrateAddonRepository.js
@@ -0,0 +1,127 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const EXPECTED_SCHEMA_VERSION = 4;
+var dbfile;
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Write out a minimal database.
+ dbfile = gProfD.clone();
+ dbfile.append("addons.sqlite");
+ let db = AM_Cc["@mozilla.org/storage/service;1"].
+ getService(AM_Ci.mozIStorageService).
+ openDatabase(dbfile);
+
+ db.createTable("addon",
+ "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "id TEXT UNIQUE, " +
+ "type TEXT, " +
+ "name TEXT, " +
+ "version TEXT, " +
+ "creator TEXT, " +
+ "creatorURL TEXT, " +
+ "description TEXT, " +
+ "fullDescription TEXT, " +
+ "developerComments TEXT, " +
+ "eula TEXT, " +
+ "iconURL TEXT, " +
+ "homepageURL TEXT, " +
+ "supportURL TEXT, " +
+ "contributionURL TEXT, " +
+ "contributionAmount TEXT, " +
+ "averageRating INTEGER, " +
+ "reviewCount INTEGER, " +
+ "reviewURL TEXT, " +
+ "totalDownloads INTEGER, " +
+ "weeklyDownloads INTEGER, " +
+ "dailyUsers INTEGER, " +
+ "sourceURI TEXT, " +
+ "repositoryStatus INTEGER, " +
+ "size INTEGER, " +
+ "updateDate INTEGER");
+
+ db.createTable("developer",
+ "addon_internal_id INTEGER, " +
+ "num INTEGER, " +
+ "name TEXT, " +
+ "url TEXT, " +
+ "PRIMARY KEY (addon_internal_id, num)");
+
+ db.createTable("screenshot",
+ "addon_internal_id INTEGER, " +
+ "num INTEGER, " +
+ "url TEXT, " +
+ "thumbnailURL TEXT, " +
+ "caption TEXT, " +
+ "PRIMARY KEY (addon_internal_id, num)");
+
+ let insertStmt = db.createStatement("INSERT INTO addon (id) VALUES (:id)");
+ insertStmt.params.id = "test1@tests.mozilla.org";
+ insertStmt.execute();
+ insertStmt.finalize();
+
+ insertStmt = db.createStatement("INSERT INTO screenshot VALUES " +
+ "(:addon_internal_id, :num, :url, :thumbnailURL, :caption)");
+
+ insertStmt.params.addon_internal_id = 1;
+ insertStmt.params.num = 0;
+ insertStmt.params.url = "http://localhost/full1-1.png";
+ insertStmt.params.thumbnailURL = "http://localhost/thumbnail1-1.png";
+ insertStmt.params.caption = "Caption 1 - 1";
+ insertStmt.execute();
+ insertStmt.finalize();
+
+ db.schemaVersion = 1;
+ db.close();
+
+
+ Services.prefs.setBoolPref("extensions.getAddons.cache.enabled", true);
+ AddonRepository.getCachedAddonByID("test1@tests.mozilla.org", function (aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_eq(aAddon.screenshots.length, 1);
+ do_check_true(aAddon.screenshots[0].width === null);
+ do_check_true(aAddon.screenshots[0].height === null);
+ do_check_true(aAddon.screenshots[0].thumbnailWidth === null);
+ do_check_true(aAddon.screenshots[0].thumbnailHeight === null);
+ do_check_eq(aAddon.iconURL, undefined);
+ do_check_eq(JSON.stringify(aAddon.icons), "{}");
+ AddonRepository.shutdown().then(
+ function checkAfterRepoShutdown() {
+ // Check the DB schema has changed once AddonRepository has freed it.
+ db = AM_Cc["@mozilla.org/storage/service;1"].
+ getService(AM_Ci.mozIStorageService).
+ openDatabase(dbfile);
+ do_check_eq(db.schemaVersion, EXPECTED_SCHEMA_VERSION);
+ do_check_true(db.indexExists("developer_idx"));
+ do_check_true(db.indexExists("screenshot_idx"));
+ do_check_true(db.indexExists("compatibility_override_idx"));
+ do_check_true(db.tableExists("compatibility_override"));
+ do_check_true(db.indexExists("icon_idx"));
+ do_check_true(db.tableExists("icon"));
+
+ // Check the trigger is working
+ db.executeSimpleSQL("INSERT INTO addon (id, type, name) VALUES('test_addon', 'extension', 'Test Addon')");
+ let internalID = db.lastInsertRowID;
+ db.executeSimpleSQL("INSERT INTO compatibility_override (addon_internal_id, num, type) VALUES('" + internalID + "', '1', 'incompatible')");
+
+ let selectStmt = db.createStatement("SELECT COUNT(*) AS count FROM compatibility_override");
+ selectStmt.executeStep();
+ do_check_eq(selectStmt.row.count, 1);
+ selectStmt.reset();
+
+ db.executeSimpleSQL("DELETE FROM addon");
+ selectStmt.executeStep();
+ do_check_eq(selectStmt.row.count, 0);
+ selectStmt.finalize();
+
+ db.close();
+ do_test_finished();
+ },
+ do_report_unexpected_exception
+ );
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_migrate_max_version.js b/toolkit/mozapps/extensions/test/xpcshell/test_migrate_max_version.js
new file mode 100644
index 000000000..171ac411e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_migrate_max_version.js
@@ -0,0 +1,103 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Checks that we don't migrate data from SQLITE if
+// the "extensions.databaseSchema" preference shows we've
+// already upgraded to JSON
+
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +
+ AddonManager.SCOPE_SYSTEM);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ writeInstallRDFForExtension(addon1, profileDir);
+
+ // Write out a minimal database
+ let dbfile = gProfD.clone();
+ dbfile.append("extensions.sqlite");
+ let db = AM_Cc["@mozilla.org/storage/service;1"].
+ getService(AM_Ci.mozIStorageService).
+ openDatabase(dbfile);
+ db.createTable("addon", "internal_id INTEGER PRIMARY KEY AUTOINCREMENT, " +
+ "id TEXT, location TEXT, version TEXT, active INTEGER, " +
+ "userDisabled INTEGER, installDate INTEGER");
+ db.createTable("targetApplication", "addon_internal_id INTEGER, " +
+ "id TEXT, minVersion TEXT, maxVersion TEXT");
+ let stmt = db.createStatement("INSERT INTO addon VALUES (NULL, :id, :location, " +
+ ":version, :active, :userDisabled, :installDate)");
+
+ let internal_ids = {};
+
+ let a = ["addon1@tests.mozilla.org", "app-profile", "1.0", "0", "1", "0"];
+ stmt.params.id = a[0];
+ stmt.params.location = a[1];
+ stmt.params.version = a[2];
+ stmt.params.active = a[3];
+ stmt.params.userDisabled = a[4];
+ stmt.params.installDate = a[5];
+ stmt.execute();
+ internal_ids[a[0]] = db.lastInsertRowID;
+ stmt.finalize();
+
+ db.schemaVersion = 14;
+ Services.prefs.setIntPref("extensions.databaseSchema", 14);
+ db.close();
+
+ startupManager();
+ run_next_test();
+}
+
+add_test(function before_rebuild() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org",
+ function check_before_rebuild (a1) {
+ // First check that it migrated OK once
+ // addon1 was disabled in the database
+ do_check_neq(a1, null);
+ do_check_true(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_false(a1.isActive);
+ do_check_false(a1.strictCompatibility);
+ do_check_false(a1.foreignInstall);
+
+ run_next_test();
+ });
+});
+
+// now shut down, remove the JSON database,
+// start up again, and make sure the data didn't migrate this time
+add_test(function rebuild_again() {
+ shutdownManager();
+ gExtensionsJSON.remove(true);
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org",
+ function check_after_rebuild(a1) {
+ // addon1 was rebuilt from extensions directory,
+ // so it appears enabled as a foreign install
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isActive);
+ do_check_false(a1.strictCompatibility);
+ do_check_true(a1.foreignInstall);
+
+ run_next_test();
+ });
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
new file mode 100644
index 000000000..4cd103fa6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_multiprocessCompatible.js
@@ -0,0 +1,120 @@
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer;
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+function build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible) {
+ return function* () {
+ dump("Running test" +
+ " multiprocessCompatible: " + multiprocessCompatible +
+ " bootstrap: " + bootstrap +
+ " updateMultiprocessCompatible: " + updateMultiprocessCompatible +
+ "\n");
+
+ let addonData = {
+ id: "addon@tests.mozilla.org",
+ name: "Test Add-on",
+ version: "1.0",
+ multiprocessCompatible,
+ bootstrap,
+ updateURL: "http://localhost:" + gPort + "/updaterdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+ }
+
+ gServer.registerPathHandler("/updaterdf", function(request, response) {
+ let updateData = {};
+ updateData[addonData.id] = [{
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+ }];
+
+ if (updateMultiprocessCompatible !== undefined) {
+ updateData[addonData.id][0].multiprocessCompatible = updateMultiprocessCompatible;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(createUpdateRDF(updateData));
+ });
+
+ let expectedMPC = updateMultiprocessCompatible === undefined ?
+ multiprocessCompatible :
+ updateMultiprocessCompatible;
+
+ let xpifile = createTempXPIFile(addonData);
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(xpifile, resolve));
+ do_check_eq(install.addon.multiprocessCompatible, !!multiprocessCompatible);
+ do_check_eq(install.addon.mpcOptedOut, multiprocessCompatible === false)
+ yield promiseCompleteAllInstalls([install]);
+
+ if (!bootstrap) {
+ yield promiseRestartManager();
+ do_check_true(isExtensionInAddonsList(profileDir, addonData.id));
+ do_check_eq(isItemMarkedMPIncompatible(addonData.id), !multiprocessCompatible);
+ }
+
+ let addon = yield promiseAddonByID(addonData.id);
+ do_check_neq(addon, null);
+ do_check_eq(addon.multiprocessCompatible, !!multiprocessCompatible);
+ do_check_eq(addon.mpcOptedOut, multiprocessCompatible === false);
+
+ yield promiseFindAddonUpdates(addon);
+
+ // Should have applied the compatibility change
+ do_check_eq(addon.multiprocessCompatible, !!expectedMPC);
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(addonData.id);
+ // Should have persisted the compatibility change
+ do_check_eq(addon.multiprocessCompatible, !!expectedMPC);
+ if (!bootstrap) {
+ do_check_true(isExtensionInAddonsList(profileDir, addonData.id));
+ do_check_eq(isItemMarkedMPIncompatible(addonData.id), !multiprocessCompatible);
+ }
+
+ addon.uninstall();
+ yield promiseRestartManager();
+
+ gServer.registerPathHandler("/updaterdf", null);
+ }
+}
+
+/* Builds a set of tests to run the same steps for every combination of:
+ * The add-on being restartless
+ * The initial add-on supporting multiprocess
+ * The update saying the add-on should or should not support multiprocess (or not say anything at all)
+ */
+for (let bootstrap of [false, true]) {
+ for (let multiprocessCompatible of [undefined, false, true]) {
+ for (let updateMultiprocessCompatible of [undefined, false, true]) {
+ add_task(build_test(multiprocessCompatible, bootstrap, updateMultiprocessCompatible));
+ }
+ }
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ startupManager();
+
+ // Create and configure the HTTP server.
+ gServer = new HttpServer();
+ gServer.registerDirectory("/data/", gTmpD);
+ gServer.start(-1);
+ gPort = gServer.identity.primaryPort;
+
+ run_next_test();
+}
+
+function end_test() {
+ gServer.stop(do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js b/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
new file mode 100644
index 000000000..a2ea5301e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_no_addons.js
@@ -0,0 +1,98 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test startup and restart when no add-ons are installed
+// bug 944006
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+// Load XPI Provider to get schema version ID
+var XPIScope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+const DB_SCHEMA = XPIScope.DB_SCHEMA;
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+function run_test() {
+ // Kick off the task-based tests...
+ run_next_test();
+}
+
+// Test for a preference to either exist with a specified value, or not exist at all
+function checkPending() {
+ try {
+ do_check_false(Services.prefs.getBoolPref("extensions.pendingOperations"));
+ }
+ catch (e) {
+ // OK
+ }
+}
+
+function checkString(aPref, aValue) {
+ try {
+ do_check_eq(Services.prefs.getCharPref(aPref), aValue)
+ }
+ catch (e) {
+ // OK
+ }
+}
+
+// Make sure all our extension state is empty/nonexistent
+function check_empty_state() {
+ do_check_false(gExtensionsJSON.exists());
+ do_check_false(gExtensionsINI.exists());
+
+ do_check_eq(Services.prefs.getIntPref("extensions.databaseSchema"), DB_SCHEMA);
+
+ checkString("extensions.bootstrappedAddons", "{}");
+ checkString("extensions.installCache", "[]");
+ checkPending();
+}
+
+// After first run with no add-ons, we expect:
+// no extensions.json is created
+// no extensions.ini
+// database schema version preference is set
+// bootstrap add-ons preference is not found
+// add-on directory state preference is an empty array
+// no pending operations
+add_task(function* first_run() {
+ startupManager();
+ check_empty_state();
+ yield true;
+});
+
+// Now do something that causes a DB load, and re-check
+function* trigger_db_load() {
+ let addonDefer = Promise.defer();
+ AddonManager.getAddonsByTypes(['extension'], addonDefer.resolve);
+ let addonList = yield addonDefer.promise;
+
+ do_check_eq(addonList.length, 0);
+ check_empty_state();
+
+ yield true;
+}
+add_task(trigger_db_load);
+
+// Now restart the manager and check again
+add_task(function* restart_and_recheck() {
+ restartManager();
+ check_empty_state();
+ yield true;
+});
+
+// and reload the DB again
+add_task(trigger_db_load);
+
+// When we start up with no DB and an old database schema, we should update the
+// schema number but not create a database
+add_task(function upgrade_schema_version() {
+ shutdownManager();
+ Services.prefs.setIntPref("extensions.databaseSchema", 1);
+
+ startupManager();
+ do_check_eq(Services.prefs.getIntPref("extensions.databaseSchema"), DB_SCHEMA);
+ check_empty_state();
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js b/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js
new file mode 100644
index 000000000..2d11e9c5b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_nodisable_hidden.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This test verifies that hidden add-ons cannot be user disabled.
+
+// for normal add-ons
+const profileDir = FileUtils.getDir("ProfD", ["extensions"]);
+// for system add-ons
+const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"], true);
+registerDirectory("XREAppFeat", distroDir);
+
+const NORMAL_ID = "normal@tests.mozilla.org";
+const SYSTEM_ID = "system@tests.mozilla.org";
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+// normal add-ons can be user disabled.
+add_task(function*() {
+
+ writeInstallRDFToDir({
+ id: NORMAL_ID,
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test disabling hidden add-ons, non-hidden add-on case.",
+ }, profileDir, NORMAL_ID);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test disabling hidden add-ons, non-hidden add-on case.");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_false(addon.userDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ // normal add-ons can be disabled by the user.
+ addon.userDisabled = true;
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test disabling hidden add-ons, non-hidden add-on case.");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.userDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ addon.uninstall();
+
+ shutdownManager();
+});
+
+// system add-ons can never be user disabled.
+add_task(function*() {
+
+ writeInstallRDFToDir({
+ id: SYSTEM_ID,
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test disabling hidden add-ons, hidden system add-on case.",
+ }, distroDir, SYSTEM_ID);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(SYSTEM_ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test disabling hidden add-ons, hidden system add-on case.");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_false(addon.userDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ // system add-ons cannot be disabled by the user.
+ try {
+ addon.userDisabled = true;
+ do_throw("Expected addon.userDisabled on a hidden add-on to throw!");
+ } catch (e) {
+ do_check_eq(e.message, `Cannot disable hidden add-on ${SYSTEM_ID}`);
+ }
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test disabling hidden add-ons, hidden system add-on case.");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_false(addon.userDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ shutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js
new file mode 100644
index 000000000..f9b7da073
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_onPropertyChanged_appDisabled.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ AddonManager.strictCompatibility = false;
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ aAddon.userDisabled = true;
+ do_execute_soon(run_test_1);
+ });
+}
+
+function run_test_1() {
+ restartManager();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_true(aAddon.userDisabled);
+ do_check_false(aAddon.isActive);
+ do_check_false(aAddon.appDisabled);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onPropertyChanged", ["appDisabled"]]
+ ]
+ }, [], run_test_2);
+
+ AddonManager.strictCompatibility = true;
+ });
+}
+
+function run_test_2() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(aAddon) {
+ do_check_neq(aAddon, null);
+ do_check_true(aAddon.userDisabled);
+ do_check_false(aAddon.isActive);
+ do_check_true(aAddon.appDisabled);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onPropertyChanged", ["appDisabled"]]
+ ]
+ }, [], callback_soon(do_test_finished));
+
+ AddonManager.strictCompatibility = false;
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js b/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js
new file mode 100644
index 000000000..c39e432bd
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_overrideblocklist.js
@@ -0,0 +1,200 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const KEY_PROFILEDIR = "ProfD";
+const KEY_APPDIR = "XCurProcD";
+const FILE_BLOCKLIST = "blocklist.xml";
+
+const PREF_BLOCKLIST_ENABLED = "extensions.blocklist.enabled";
+
+const OLD = do_get_file("data/test_overrideblocklist/old.xml");
+const NEW = do_get_file("data/test_overrideblocklist/new.xml");
+const ANCIENT = do_get_file("data/test_overrideblocklist/ancient.xml");
+const OLD_TSTAMP = 1296046918000;
+const NEW_TSTAMP = 1396046918000;
+
+const gAppDir = FileUtils.getFile(KEY_APPDIR, []);
+
+var oldAddon = {
+ id: "old@tests.mozilla.org",
+ version: 1
+}
+var newAddon = {
+ id: "new@tests.mozilla.org",
+ version: 1
+}
+var ancientAddon = {
+ id: "ancient@tests.mozilla.org",
+ version: 1
+}
+var invalidAddon = {
+ id: "invalid@tests.mozilla.org",
+ version: 1
+}
+
+function incrementAppVersion() {
+ gAppInfo.version = "" + (parseInt(gAppInfo.version) + 1);
+}
+
+function clearBlocklists() {
+ let blocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ if (blocklist.exists())
+ blocklist.remove(true);
+
+ blocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+ if (blocklist.exists())
+ blocklist.remove(true);
+}
+
+function reloadBlocklist() {
+ Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_BLOCKLIST_ENABLED, true);
+}
+
+function copyToApp(file) {
+ file.clone().copyTo(gAppDir, FILE_BLOCKLIST);
+}
+
+function copyToProfile(file, tstamp) {
+ file = file.clone();
+ file.copyTo(gProfD, FILE_BLOCKLIST);
+ file = gProfD.clone();
+ file.append(FILE_BLOCKLIST);
+ file.lastModifiedTime = tstamp;
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ let appBlocklist = FileUtils.getFile(KEY_APPDIR, [FILE_BLOCKLIST]);
+ if (appBlocklist.exists()) {
+ try {
+ appBlocklist.moveTo(gAppDir, "blocklist.old");
+ }
+ catch (e) {
+ todo(false, "Aborting test due to unmovable blocklist file: " + e);
+ return;
+ }
+ do_register_cleanup(function() {
+ clearBlocklists();
+ appBlocklist.moveTo(gAppDir, FILE_BLOCKLIST);
+ });
+ }
+
+ run_next_test();
+}
+
+// On first run whataver is in the app dir should get copied to the profile
+add_test(function test_copy() {
+ clearBlocklists();
+ copyToApp(OLD);
+
+ incrementAppVersion();
+ startupManager();
+
+ reloadBlocklist();
+ let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsIBlocklistService);
+ do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+ do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+ do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+ do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+ shutdownManager();
+
+ run_next_test();
+});
+
+// An ancient blocklist should be ignored
+add_test(function test_ancient() {
+ clearBlocklists();
+ copyToApp(ANCIENT);
+ copyToProfile(OLD, OLD_TSTAMP);
+
+ incrementAppVersion();
+ startupManager();
+
+ reloadBlocklist();
+ let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsIBlocklistService);
+ do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+ do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+ do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+ do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+ shutdownManager();
+
+ run_next_test();
+});
+
+// A new blocklist should override an old blocklist
+add_test(function test_override() {
+ clearBlocklists();
+ copyToApp(NEW);
+ copyToProfile(OLD, OLD_TSTAMP);
+
+ incrementAppVersion();
+ startupManager();
+
+ reloadBlocklist();
+ let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsIBlocklistService);
+ do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+ do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+ do_check_false(blocklist.isAddonBlocklisted(oldAddon));
+ do_check_true(blocklist.isAddonBlocklisted(newAddon));
+
+ shutdownManager();
+
+ run_next_test();
+});
+
+// An old blocklist shouldn't override a new blocklist
+add_test(function test_retain() {
+ clearBlocklists();
+ copyToApp(OLD);
+ copyToProfile(NEW, NEW_TSTAMP);
+
+ incrementAppVersion();
+ startupManager();
+
+ reloadBlocklist();
+ let blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsIBlocklistService);
+ do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+ do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+ do_check_false(blocklist.isAddonBlocklisted(oldAddon));
+ do_check_true(blocklist.isAddonBlocklisted(newAddon));
+
+ shutdownManager();
+
+ run_next_test();
+});
+
+// A missing blocklist in the profile should still load an app-shipped blocklist
+add_test(function test_missing() {
+ clearBlocklists();
+ copyToApp(OLD);
+ copyToProfile(NEW, NEW_TSTAMP);
+
+ incrementAppVersion();
+ startupManager();
+ shutdownManager();
+
+ let blocklist = FileUtils.getFile(KEY_PROFILEDIR, [FILE_BLOCKLIST]);
+ blocklist.remove(true);
+ startupManager(false);
+
+ reloadBlocklist();
+ blocklist = AM_Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(AM_Ci.nsIBlocklistService);
+ do_check_false(blocklist.isAddonBlocklisted(invalidAddon));
+ do_check_false(blocklist.isAddonBlocklisted(ancientAddon));
+ do_check_true(blocklist.isAddonBlocklisted(oldAddon));
+ do_check_false(blocklist.isAddonBlocklisted(newAddon));
+
+ shutdownManager();
+
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pass_symbol.js b/toolkit/mozapps/extensions/test/xpcshell/test_pass_symbol.js
new file mode 100644
index 000000000..657601e45
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pass_symbol.js
@@ -0,0 +1,43 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const PASS_PREF = "symboltest.instanceid.pass";
+const FAIL_BOGUS_PREF = "symboltest.instanceid.fail_bogus";
+const FAIL_ID_PREF = "symboltest.instanceid.fail_bogus";
+const ADDON_ID = "test_symbol@tests.mozilla.org";
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+BootstrapMonitor.init();
+
+// symbol is passed when add-on is installed
+add_task(function*() {
+ for (let pref of [PASS_PREF, FAIL_BOGUS_PREF, FAIL_ID_PREF])
+ Services.prefs.clearUserPref(pref);
+
+ yield promiseInstallAllFiles([do_get_addon("test_symbol")], true);
+
+ let addon = yield promiseAddonByID(ADDON_ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Symbol");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+
+ // most of the test is in bootstrap.js in the addon because BootstrapMonitor
+ // currently requires the objects in `data` to be serializable, and we
+ // need a real reference to the symbol to test this.
+ do_execute_soon(function() {
+ // give the startup time to run
+ do_check_true(Services.prefs.getBoolPref(PASS_PREF));
+ do_check_true(Services.prefs.getBoolPref(FAIL_BOGUS_PREF));
+ do_check_true(Services.prefs.getBoolPref(FAIL_ID_PREF));
+ });
+
+ yield promiseRestartManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js b/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
new file mode 100644
index 000000000..48fef406f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+// Checks that permissions set in preferences are correctly imported but can
+// be removed by the user.
+
+const XPI_MIMETYPE = "application/x-xpinstall";
+
+function newPrincipal(uri) {
+ return Services.scriptSecurityManager.createCodebasePrincipal(NetUtil.newURI(uri), {});
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ Services.prefs.setCharPref("xpinstall.whitelist.add", "https://test1.com,https://test2.com");
+ Services.prefs.setCharPref("xpinstall.whitelist.add.36", "https://test3.com,https://www.test4.com");
+ Services.prefs.setCharPref("xpinstall.whitelist.add.test5", "https://test5.com");
+
+ Services.perms.add(NetUtil.newURI("https://www.test9.com"), "install",
+ AM_Ci.nsIPermissionManager.ALLOW_ACTION);
+
+ startupManager();
+
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("http://test1.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test1.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test2.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test3.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test4.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test4.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("http://www.test5.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test5.com")));
+
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("http://www.test6.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test6.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test7.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test8.com")));
+
+ // This should remain unaffected
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("http://www.test9.com")));
+ do_check_true(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test9.com")));
+
+ Services.perms.removeAll();
+
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test1.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test2.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test3.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test4.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test5.com")));
+
+ // Upgrade the application and verify that the permissions are still not there
+ restartManager("2");
+
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test1.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test2.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://test3.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test4.com")));
+ do_check_false(AddonManager.isInstallAllowed(XPI_MIMETYPE,
+ newPrincipal("https://www.test5.com")));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
new file mode 100644
index 000000000..576f04a65
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_permissions_prefs.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that xpinstall.[whitelist|blacklist].add preferences are emptied when
+// converted into permissions.
+
+const PREF_XPI_WHITELIST_PERMISSIONS = "xpinstall.whitelist.add";
+const PREF_XPI_BLACKLIST_PERMISSIONS = "xpinstall.blacklist.add";
+
+function newPrincipal(uri) {
+ return Services.scriptSecurityManager.createCodebasePrincipal(NetUtil.newURI(uri), {});
+}
+
+function do_check_permission_prefs(preferences) {
+ // Check preferences were emptied
+ for (let pref of preferences) {
+ try {
+ do_check_eq(Services.prefs.getCharPref(pref), "");
+ }
+ catch (e) {
+ // Successfully emptied
+ }
+ }
+}
+
+function clear_imported_preferences_cache() {
+ let scope = Components.utils.import("resource://gre/modules/PermissionsUtils.jsm", {});
+ scope.gImportedPrefBranches.clear();
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ // Create own preferences to test
+ Services.prefs.setCharPref("xpinstall.whitelist.add.EMPTY", "");
+ Services.prefs.setCharPref("xpinstall.whitelist.add.TEST", "http://whitelist.example.com");
+ Services.prefs.setCharPref("xpinstall.blacklist.add.EMPTY", "");
+ Services.prefs.setCharPref("xpinstall.blacklist.add.TEST", "http://blacklist.example.com");
+
+ // Get list of preferences to check
+ var whitelistPreferences = Services.prefs.getChildList(PREF_XPI_WHITELIST_PERMISSIONS, {});
+ var blacklistPreferences = Services.prefs.getChildList(PREF_XPI_BLACKLIST_PERMISSIONS, {});
+ var preferences = whitelistPreferences.concat(blacklistPreferences);
+
+ startupManager();
+
+ // Permissions are imported lazily - act as thought we're checking an install,
+ // to trigger on-deman importing of the permissions.
+ AddonManager.isInstallAllowed("application/x-xpinstall", newPrincipal("http://example.com/file.xpi"));
+ do_check_permission_prefs(preferences);
+
+
+ // Import can also be triggerred by an observer notification by any other area
+ // of code, such as a permissions management UI.
+
+ // First, request to flush all permissions
+ clear_imported_preferences_cache();
+ Services.prefs.setCharPref("xpinstall.whitelist.add.TEST2", "https://whitelist2.example.com");
+ Services.obs.notifyObservers(null, "flush-pending-permissions", "install");
+ do_check_permission_prefs(preferences);
+
+ // Then, request to flush just install permissions
+ clear_imported_preferences_cache();
+ Services.prefs.setCharPref("xpinstall.whitelist.add.TEST3", "https://whitelist3.example.com");
+ Services.obs.notifyObservers(null, "flush-pending-permissions", "");
+ do_check_permission_prefs(preferences);
+
+ // And a request to flush some other permissions sholdn't flush install permissions
+ clear_imported_preferences_cache();
+ Services.prefs.setCharPref("xpinstall.whitelist.add.TEST4", "https://whitelist4.example.com");
+ Services.obs.notifyObservers(null, "flush-pending-permissions", "lolcats");
+ do_check_eq(Services.prefs.getCharPref("xpinstall.whitelist.add.TEST4"), "https://whitelist4.example.com");
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js b/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
new file mode 100644
index 000000000..563b7434c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginBlocklistCtp.js
@@ -0,0 +1,182 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+const nsIBLS = Components.interfaces.nsIBlocklistService;
+Components.utils.import("resource://testing-common/httpd.js");
+
+var gBlocklistService = null;
+var gNotifier = null;
+var gNextTest = null;
+var gPluginHost = null;
+
+var gServer = new HttpServer();
+gServer.start(-1);
+gPort = gServer.identity.primaryPort;
+mapFile("/data/test_pluginBlocklistCtp.xml", gServer);
+mapFile("/data/test_pluginBlocklistCtpUndo.xml", gServer);
+
+var PLUGINS = [{
+ // severity=0, vulnerabilitystatus=0 -> outdated
+ name: "test_plugin_0",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // severity=0, vulnerabilitystatus=1 -> update available
+ name: "test_plugin_1",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // severity=0, vulnerabilitystatus=2 -> no update
+ name: "test_plugin_2",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // no severity field -> severity=3 by default -> hardblock
+ name: "test_plugin_3",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // severity=1, vulnerabilitystatus=2 -> softblock
+ name: "test_plugin_4",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+},
+{
+ // not in the blocklist -> not blocked
+ name: "test_plugin_5",
+ version: "5",
+ disabled: false,
+ blocklisted: false
+}];
+
+function test_basic() {
+ var blocklist = Components.classes["@mozilla.org/extensions/blocklist;1"].getService(nsIBLS);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[0], "1", "1.9") == nsIBLS.STATE_OUTDATED);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[1], "1", "1.9") == nsIBLS.STATE_VULNERABLE_UPDATE_AVAILABLE);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[2], "1", "1.9") == nsIBLS.STATE_VULNERABLE_NO_UPDATE);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[3], "1", "1.9") == nsIBLS.STATE_BLOCKED);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[4], "1", "1.9") == nsIBLS.STATE_SOFTBLOCKED);
+
+ do_check_true(blocklist.getPluginBlocklistState(PLUGINS[5], "1", "1.9") == nsIBLS.STATE_NOT_BLOCKED);
+
+ gNextTest = test_is_not_clicktoplay;
+ do_execute_soon(gNextTest);
+}
+
+function get_test_plugin() {
+ var pluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+ for (var plugin of pluginHost.getPluginTags()) {
+ if (plugin.name == "Test Plug-in")
+ return plugin;
+ }
+ do_check_true(false);
+ return null;
+}
+
+// At this time, the blocklist does not have an entry for the test plugin,
+// so it shouldn't be click-to-play.
+function test_is_not_clicktoplay() {
+ var plugin = get_test_plugin();
+ var blocklistState = gBlocklistService.getPluginBlocklistState(plugin, "1", "1.9");
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/test_pluginBlocklistCtpUndo.xml");
+ gNextTest = test_is_clicktoplay;
+ gNotifier.notify(null);
+}
+
+// Here, we've updated the blocklist to have a block for the test plugin,
+// so it should be click-to-play.
+function test_is_clicktoplay() {
+ var plugin = get_test_plugin();
+ var blocklistState = gBlocklistService.getPluginBlocklistState(plugin, "1", "1.9");
+ do_check_eq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/test_pluginBlocklistCtp.xml");
+ gNextTest = test_is_not_clicktoplay2;
+ gNotifier.notify(null);
+}
+
+// But now we've removed that entry from the blocklist (really we've gone back
+// to the old one), so the plugin shouldn't be click-to-play any more.
+function test_is_not_clicktoplay2() {
+ var plugin = get_test_plugin();
+ var blocklistState = gBlocklistService.getPluginBlocklistState(plugin, "1", "1.9");
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/test_pluginBlocklistCtpUndo.xml");
+ gNextTest = test_disable_blocklist;
+ gNotifier.notify(null);
+}
+
+// Test that disabling the blocklist when a plugin is ctp-blocklisted will
+// result in the plugin not being click-to-play.
+function test_disable_blocklist() {
+ var plugin = get_test_plugin();
+ var blocklistState = gBlocklistService.getPluginBlocklistState(plugin, "1", "1.9");
+ do_check_eq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
+
+ gNextTest = null;
+ Services.prefs.setBoolPref("extensions.blocklist.enabled", false);
+ blocklistState = gBlocklistService.getPluginBlocklistState(plugin, "1", "1.9");
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_NO_UPDATE);
+ do_check_neq(blocklistState, Components.interfaces.nsIBlocklistService.STATE_VULNERABLE_UPDATE_AVAILABLE);
+
+ // it should still be possible to make a plugin click-to-play via the pref
+ // and setting that plugin's enabled state to click-to-play
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ let previousEnabledState = plugin.enabledState;
+ plugin.enabledState = Components.interfaces.nsIPluginTag.STATE_CLICKTOPLAY;
+ do_check_eq(gPluginHost.getStateForType("application/x-test"), Components.interfaces.nsIPluginTag.STATE_CLICKTOPLAY);
+ // clean up plugin state
+ plugin.enabledState = previousEnabledState;
+
+ gServer.stop(do_test_finished);
+}
+
+// Observe "blocklist-updated" so we know when to advance to the next test
+function observer() {
+ if (gNextTest)
+ do_execute_soon(gNextTest);
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+
+ Services.prefs.setCharPref("extensions.blocklist.url", "http://localhost:" + gPort + "/data/test_pluginBlocklistCtp.xml");
+ Services.prefs.setBoolPref("plugin.load_flash_only", false);
+ startupManager();
+
+ gPluginHost = Components.classes["@mozilla.org/plugin/host;1"].getService(Components.interfaces.nsIPluginHost);
+ gBlocklistService = Components.classes["@mozilla.org/extensions/blocklist;1"].getService(Components.interfaces.nsIBlocklistService);
+ gNotifier = Components.classes["@mozilla.org/extensions/blocklist;1"].getService(Components.interfaces.nsITimerCallback);
+ Services.obs.addObserver(observer, "blocklist-updated", false);
+
+ do_register_cleanup(function() {
+ Services.prefs.clearUserPref("extensions.blocklist.url");
+ Services.prefs.clearUserPref("extensions.blocklist.enabled");
+ Services.prefs.clearUserPref("plugins.click_to_play");
+ Services.obs.removeObserver(observer, "blocklist-updated");
+ });
+
+ gNextTest = test_basic;
+ do_test_pending();
+ gNotifier.notify(null);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js b/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
new file mode 100644
index 000000000..e5acb28a4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginInfoURL.js
@@ -0,0 +1,90 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var Ci = Components.interfaces;
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+/**
+ * MockPlugin mimics the behaviour of a plugin.
+ */
+function MockPlugin(name, version, enabledState) {
+ this.name = name;
+ this.version = version;
+ this.enabledState = enabledState;
+}
+
+MockPlugin.prototype = {
+ get blocklisted() {
+ let bls = Services.blocklist;
+ return bls.getPluginBlocklistState(this) == bls.STATE_BLOCKED;
+ },
+
+ get disabled() {
+ return this.enabledState == Ci.nsIPluginTag.STATE_DISABLED;
+ }
+};
+
+// The mocked blocked plugin used to test the blocklist.
+const PLUGINS = [
+ new MockPlugin('test_with_infoURL', '5', Ci.nsIPluginTag.STATE_ENABLED),
+ new MockPlugin('test_with_altInfoURL', '5', Ci.nsIPluginTag.STATE_ENABLED),
+ new MockPlugin('test_no_infoURL', '5', Ci.nsIPluginTag.STATE_ENABLED),
+ new MockPlugin('test_newVersion', '1', Ci.nsIPluginTag.STATE_ENABLED),
+ new MockPlugin('test_newVersion', '3', Ci.nsIPluginTag.STATE_ENABLED)
+];
+
+/**
+ * The entry point of the unit tests, which is also responsible of
+ * copying the blocklist file to the profile folder.
+ */
+function run_test() {
+ copyBlocklistToProfile(do_get_file('data/pluginInfoURL_block.xml'));
+
+ createAppInfo('xpcshell@tests.mozilla.org', 'XPCShell', '3', '8');
+ startupManager();
+
+ run_next_test();
+}
+
+/**
+ * Test that the blocklist service correctly loads and returns the infoURL for
+ * a plugin that matches the first entry in the blocklist.
+ */
+add_task(function* test_infoURL() {
+ // The testInfoURL must match the value within the
+ // <infoURL> tag in pluginInfoURL_block.xml.
+ let testInfoURL = 'http://test.url.com/';
+
+ Assert.strictEqual(Services.blocklist.getPluginInfoURL(PLUGINS[0]),
+ testInfoURL, 'Should be the provided url when an infoURL tag is available');
+});
+
+/**
+ * Test that the blocklist service correctly loads and returns the infoURL for
+ * a plugin that partially matches an earlier entry in the blocklist.
+ */
+add_task(function* test_altInfoURL() {
+ let altTestInfoURL = 'http://alt.test.url.com/';
+
+ Assert.strictEqual(Services.blocklist.getPluginInfoURL(PLUGINS[1]),
+ altTestInfoURL, 'Should be the alternative infoURL');
+});
+
+/**
+ * Test that the blocklist service correctly returns null
+ * if the infoURL tag is missing in the blocklist.xml file.
+ */
+add_task(function* test_infoURL_missing() {
+ Assert.strictEqual(Services.blocklist.getPluginInfoURL(PLUGINS[2]), null,
+ 'Should be null when no infoURL tag is available.');
+});
+
+add_task(function* test_intoURL_newVersion() {
+ let testInfoURL = 'http://test.url2.com/';
+ Assert.strictEqual(Services.blocklist.getPluginInfoURL(PLUGINS[3]),
+ testInfoURL, 'Old plugin should match');
+ Assert.strictEqual(Services.blocklist.getPluginInfoURL(PLUGINS[4]),
+ null, 'New plugin should not match');
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pluginchange.js b/toolkit/mozapps/extensions/test/xpcshell/test_pluginchange.js
new file mode 100644
index 000000000..05e17b35e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pluginchange.js
@@ -0,0 +1,283 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const LIST_UPDATED_TOPIC = "plugins-list-updated";
+
+// We need to use the same algorithm for generating IDs for plugins
+var { getIDHashForString } = Components.utils.import("resource://gre/modules/addons/PluginProvider.jsm");
+var { MockRegistrar } = Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+function PluginTag(name, description) {
+ this.name = name;
+ this.description = description;
+}
+
+PluginTag.prototype = {
+ name: null,
+ description: null,
+ version: "1.0",
+ filename: null,
+ fullpath: null,
+ disabled: false,
+ blocklisted: false,
+ clicktoplay: false,
+
+ mimeTypes: [],
+
+ getMimeTypes: function(count) {
+ count.value = this.mimeTypes.length;
+ return this.mimeTypes;
+ }
+};
+
+const PLUGINS = [
+ // A standalone plugin
+ new PluginTag("Java", "A mock Java plugin"),
+
+ // A plugin made up of two plugin files
+ new PluginTag("Flash", "A mock Flash plugin"),
+ new PluginTag("Flash", "A mock Flash plugin")
+];
+
+const gPluginHost = {
+ // nsIPluginHost
+ getPluginTags: function(count) {
+ count.value = PLUGINS.length;
+ return PLUGINS;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([AM_Ci.nsIPluginHost])
+};
+
+MockRegistrar.register("@mozilla.org/plugin/host;1", gPluginHost);
+
+// This verifies that when the list of plugins changes the add-ons manager
+// correctly updates
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ Services.prefs.setBoolPref("media.gmp-provider.enabled", false);
+
+ startupManager();
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ run_test_1();
+}
+
+function end_test() {
+ do_execute_soon(do_test_finished);
+}
+
+function sortAddons(addons) {
+ addons.sort(function(a, b) {
+ return a.name.localeCompare(b.name);
+ });
+}
+
+// Basic check that the mock object works
+function run_test_1() {
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Java");
+ do_check_false(addons[1].userDisabled);
+
+ run_test_2();
+ });
+}
+
+// No change to the list should not trigger any events or changes in the API
+function run_test_2() {
+ // Reorder the list a bit
+ let tag = PLUGINS[0];
+ PLUGINS[0] = PLUGINS[2];
+ PLUGINS[2] = PLUGINS[1];
+ PLUGINS[1] = tag;
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Java");
+ do_check_false(addons[1].userDisabled);
+
+ run_test_3();
+ });
+}
+
+// Tests that a newly detected plugin shows up in the API and sends out events
+function run_test_3() {
+ let tag = new PluginTag("Quicktime", "A mock Quicktime plugin");
+ PLUGINS.push(tag);
+ let id = getIDHashForString(tag.name + tag.description);
+
+ let test_params = {};
+ test_params[id] = [
+ ["onInstalling", false],
+ "onInstalled"
+ ];
+
+ prepare_test(test_params, [
+ "onExternalInstall"
+ ]);
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 3);
+
+ do_check_eq(addons[0].name, "Flash");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Java");
+ do_check_false(addons[1].userDisabled);
+ do_check_eq(addons[2].name, "Quicktime");
+ do_check_false(addons[2].userDisabled);
+
+ run_test_4();
+ });
+}
+
+// Tests that a removed plugin disappears from in the API and sends out events
+function run_test_4() {
+ let tag = PLUGINS.splice(1, 1)[0];
+ let id = getIDHashForString(tag.name + tag.description);
+
+ let test_params = {};
+ test_params[id] = [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ];
+
+ prepare_test(test_params);
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Quicktime");
+ do_check_false(addons[1].userDisabled);
+
+ run_test_5();
+ });
+}
+
+// Removing part of the flash plugin should have no effect
+function run_test_5() {
+ PLUGINS.splice(0, 1);
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Quicktime");
+ do_check_false(addons[1].userDisabled);
+
+ run_test_6();
+ });
+}
+
+// Replacing flash should be detected
+function run_test_6() {
+ let oldTag = PLUGINS.splice(0, 1)[0];
+ let newTag = new PluginTag("Flash 2", "A new crash-free Flash!");
+ newTag.disabled = true;
+ PLUGINS.push(newTag);
+
+ let test_params = {};
+ test_params[getIDHashForString(oldTag.name + oldTag.description)] = [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ];
+ test_params[getIDHashForString(newTag.name + newTag.description)] = [
+ ["onInstalling", false],
+ "onInstalled"
+ ];
+
+ prepare_test(test_params, [
+ "onExternalInstall"
+ ]);
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash 2");
+ do_check_true(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Quicktime");
+ do_check_false(addons[1].userDisabled);
+
+ run_test_7();
+ });
+}
+
+// If new tags are detected and the disabled state changes then we should send
+// out appropriate notifications
+function run_test_7() {
+ PLUGINS[0] = new PluginTag("Quicktime", "A mock Quicktime plugin");
+ PLUGINS[0].disabled = true;
+ PLUGINS[1] = new PluginTag("Flash 2", "A new crash-free Flash!");
+
+ let test_params = {};
+ test_params[getIDHashForString(PLUGINS[0].name + PLUGINS[0].description)] = [
+ ["onDisabling", false],
+ "onDisabled"
+ ];
+ test_params[getIDHashForString(PLUGINS[1].name + PLUGINS[1].description)] = [
+ ["onEnabling", false],
+ "onEnabled"
+ ];
+
+ prepare_test(test_params);
+
+ Services.obs.notifyObservers(null, LIST_UPDATED_TOPIC, null);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ sortAddons(addons);
+
+ do_check_eq(addons.length, 2);
+
+ do_check_eq(addons[0].name, "Flash 2");
+ do_check_false(addons[0].userDisabled);
+ do_check_eq(addons[1].name, "Quicktime");
+ do_check_true(addons[1].userDisabled);
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_plugins.js b/toolkit/mozapps/extensions/test/xpcshell/test_plugins.js
new file mode 100644
index 000000000..3f0ac7ebe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_plugins.js
@@ -0,0 +1,210 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var TEST_PLUGIN_DESCRIPTION = "Flash plug-in for testing purposes.";
+
+// This verifies that plugins exist and can be enabled and disabled.
+var gID = null;
+
+function setTestPluginState(state) {
+ let tags = AM_Cc["@mozilla.org/plugin/host;1"].getService(AM_Ci.nsIPluginHost)
+ .getPluginTags();
+ for (let tag of tags) {
+ do_print("Checking tag: " + tag.description);
+ if (tag.description == TEST_PLUGIN_DESCRIPTION) {
+ tag.enabledState = state;
+ return;
+ }
+ }
+ throw Error("No plugin tag found for the test plugin");
+}
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ Services.prefs.setBoolPref("plugins.click_to_play", true);
+ Services.prefs.setBoolPref("plugin.load_flash_only", false);
+
+ setTestPluginState(AM_Ci.nsIPluginTag.STATE_CLICKTOPLAY);
+
+ startupManager();
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ run_test_1();
+}
+
+// Finds the test plugin library
+function get_test_plugin() {
+ var pluginEnum = Services.dirsvc.get("APluginsDL", AM_Ci.nsISimpleEnumerator);
+ while (pluginEnum.hasMoreElements()) {
+ let dir = pluginEnum.getNext().QueryInterface(AM_Ci.nsILocalFile);
+ let plugin = dir.clone();
+ // OSX plugin
+ plugin.append("npswftest.plugin");
+ if (plugin.exists()) {
+ plugin.normalize();
+ return plugin;
+ }
+ plugin = dir.clone();
+ // *nix plugin
+ plugin.append("libnpswftest.so");
+ if (plugin.exists()) {
+ plugin.normalize();
+ return plugin;
+ }
+ // Windows plugin
+ plugin = dir.clone();
+ plugin.append("npswftest.dll");
+ if (plugin.exists()) {
+ plugin.normalize();
+ return plugin;
+ }
+ }
+ return null;
+}
+
+function getFileSize(aFile) {
+ if (!aFile.isDirectory())
+ return aFile.fileSize;
+
+ let size = 0;
+ let entries = aFile.directoryEntries.QueryInterface(AM_Ci.nsIDirectoryEnumerator);
+ let entry;
+ while (entry = entries.nextFile)
+ size += getFileSize(entry);
+ entries.close();
+ return size;
+}
+
+function getPluginLastModifiedTime(aPluginFile) {
+ // On OS X we use the bundle contents last modified time as using
+ // the package directories modified date may be outdated.
+ // See bug 313700.
+ try {
+ let localFileMac = aPluginFile.QueryInterface(AM_Ci.nsILocalFileMac);
+ if (localFileMac) {
+ return localFileMac.bundleContentsLastModifiedTime;
+ }
+ } catch (e) {
+ }
+
+ return aPluginFile.lastModifiedTime;
+}
+
+// Tests that the test plugin exists
+function run_test_1() {
+ var testPlugin = get_test_plugin();
+ do_check_neq(testPlugin, null);
+
+ AddonManager.getAddonsByTypes(["plugin"], function(addons) {
+ do_check_true(addons.length > 0);
+
+ addons.forEach(function(p) {
+ if (p.description == TEST_PLUGIN_DESCRIPTION)
+ gID = p.id;
+ });
+
+ do_check_neq(gID, null);
+
+ AddonManager.getAddonByID(gID, function(p) {
+ do_check_neq(p, null);
+ do_check_eq(p.name, "Shockwave Flash");
+ do_check_eq(p.description, TEST_PLUGIN_DESCRIPTION);
+ do_check_eq(p.creator, null);
+ do_check_eq(p.version, "1.0.0.0");
+ do_check_eq(p.type, "plugin");
+ do_check_eq(p.userDisabled, "askToActivate");
+ do_check_false(p.appDisabled);
+ do_check_true(p.isActive);
+ do_check_true(p.isCompatible);
+ do_check_true(p.providesUpdatesSecurely);
+ do_check_eq(p.blocklistState, 0);
+ do_check_eq(p.permissions, AddonManager.PERM_CAN_DISABLE | AddonManager.PERM_CAN_ENABLE);
+ do_check_eq(p.pendingOperations, 0);
+ do_check_true(p.size > 0);
+ do_check_eq(p.size, getFileSize(testPlugin));
+ do_check_true(p.updateDate > 0);
+ do_check_true("isCompatibleWith" in p);
+ do_check_true("findUpdates" in p);
+
+ let lastModifiedTime = getPluginLastModifiedTime(testPlugin);
+ do_check_eq(p.updateDate.getTime(), lastModifiedTime);
+ do_check_eq(p.installDate.getTime(), lastModifiedTime);
+
+ run_test_2(p);
+ });
+ });
+}
+
+// Tests that disabling a plugin works
+function run_test_2(p) {
+ let test = {};
+ test[gID] = [
+ ["onDisabling", false],
+ "onDisabled",
+ ["onPropertyChanged", ["userDisabled"]]
+ ];
+ prepare_test(test);
+
+ p.userDisabled = true;
+
+ ensure_test_completed();
+
+ do_check_true(p.userDisabled);
+ do_check_false(p.appDisabled);
+ do_check_false(p.isActive);
+
+ AddonManager.getAddonByID(gID, function(p2) {
+ do_check_neq(p2, null);
+ do_check_true(p2.userDisabled);
+ do_check_false(p2.appDisabled);
+ do_check_false(p2.isActive);
+ do_check_eq(p2.name, "Shockwave Flash");
+
+ run_test_3(p2);
+ });
+}
+
+// Tests that enabling a plugin works
+function run_test_3(p) {
+ let test = {};
+ test[gID] = [
+ ["onEnabling", false],
+ "onEnabled"
+ ];
+ prepare_test(test);
+
+ p.userDisabled = false;
+
+ ensure_test_completed();
+
+ do_check_false(p.userDisabled);
+ do_check_false(p.appDisabled);
+ do_check_true(p.isActive);
+
+ AddonManager.getAddonByID(gID, function(p2) {
+ do_check_neq(p2, null);
+ do_check_false(p2.userDisabled);
+ do_check_false(p2.appDisabled);
+ do_check_true(p2.isActive);
+ do_check_eq(p2.name, "Shockwave Flash");
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Verify that after a restart the test plugin has the same ID
+function run_test_4() {
+ restartManager();
+
+ AddonManager.getAddonByID(gID, function(p) {
+ do_check_neq(p, null);
+ do_check_eq(p.name, "Shockwave Flash");
+
+ Services.prefs.clearUserPref("plugins.click_to_play");
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js b/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js
new file mode 100644
index 000000000..c6a10e7c1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_pref_properties.js
@@ -0,0 +1,221 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests the preference-related properties of AddonManager
+// eg: AddonManager.checkCompatibility, AddonManager.updateEnabled, etc
+
+var gManagerEventsListener = {
+ seenEvents: [],
+ init: function() {
+ let events = ["onCompatibilityModeChanged", "onCheckUpdateSecurityChanged",
+ "onUpdateModeChanged"];
+ events.forEach(function(aEvent) {
+ this[aEvent] = function() {
+ do_print("Saw event " + aEvent);
+ this.seenEvents.push(aEvent);
+ }
+ }, this);
+ AddonManager.addManagerListener(this);
+ // Try to add twice, to test that the second time silently fails.
+ AddonManager.addManagerListener(this);
+ },
+ shutdown: function() {
+ AddonManager.removeManagerListener(this);
+ },
+ expect: function(aEvents) {
+ this.expectedEvents = aEvents;
+ },
+ checkExpected: function() {
+ do_print("Checking expected events...");
+ while (this.expectedEvents.length > 0) {
+ let event = this.expectedEvents.pop();
+ do_print("Looking for expected event " + event);
+ let matchingEvents = this.seenEvents.filter(function(aSeenEvent) {
+ return aSeenEvent == event;
+ });
+ do_check_eq(matchingEvents.length, 1);
+ }
+ this.seenEvents = [];
+ }
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ Services.prefs.setBoolPref("extensions.update.enabled", true);
+ Services.prefs.setBoolPref("extensions.update.autoUpdateDefault", true);
+ Services.prefs.setBoolPref("extensions.strictCompatibility", true);
+ Services.prefs.setBoolPref("extensions.checkUpdatesecurity", true);
+
+ startupManager();
+ gManagerEventsListener.init();
+
+
+ // AddonManager.updateEnabled
+ gManagerEventsListener.expect(["onUpdateModeChanged"]);
+ AddonManager.updateEnabled = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.updateEnabled);
+ do_check_false(Services.prefs.getBoolPref("extensions.update.enabled"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.updateEnabled = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.updateEnabled);
+ do_check_false(Services.prefs.getBoolPref("extensions.update.enabled"));
+
+ gManagerEventsListener.expect(["onUpdateModeChanged"]);
+ AddonManager.updateEnabled = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.updateEnabled);
+ do_check_true(Services.prefs.getBoolPref("extensions.update.enabled"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.updateEnabled = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.updateEnabled);
+ do_check_true(Services.prefs.getBoolPref("extensions.update.enabled"));
+
+ // AddonManager.autoUpdateDefault
+ gManagerEventsListener.expect(["onUpdateModeChanged"]);
+ AddonManager.autoUpdateDefault = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.autoUpdateDefault);
+ do_check_false(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.autoUpdateDefault = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.autoUpdateDefault);
+ do_check_false(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault"));
+
+ gManagerEventsListener.expect(["onUpdateModeChanged"]);
+ AddonManager.autoUpdateDefault = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.autoUpdateDefault);
+ do_check_true(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.autoUpdateDefault = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.autoUpdateDefault);
+ do_check_true(Services.prefs.getBoolPref("extensions.update.autoUpdateDefault"));
+
+ // AddonManager.strictCompatibility
+ gManagerEventsListener.expect(["onCompatibilityModeChanged"]);
+ AddonManager.strictCompatibility = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.strictCompatibility);
+ do_check_false(Services.prefs.getBoolPref("extensions.strictCompatibility"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.strictCompatibility = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.strictCompatibility);
+ do_check_false(Services.prefs.getBoolPref("extensions.strictCompatibility"));
+
+ gManagerEventsListener.expect(["onCompatibilityModeChanged"]);
+ AddonManager.strictCompatibility = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.strictCompatibility);
+ do_check_true(Services.prefs.getBoolPref("extensions.strictCompatibility"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.strictCompatibility = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.strictCompatibility);
+ do_check_true(Services.prefs.getBoolPref("extensions.strictCompatibility"));
+
+
+ // AddonManager.checkCompatibility
+ if (isNightlyChannel()) {
+ var version = "nightly";
+ } else {
+ version = Services.appinfo.version.replace(/^([^\.]+\.[0-9]+[a-z]*).*/gi, "$1");
+ }
+ const COMPATIBILITY_PREF = "extensions.checkCompatibility." + version;
+
+ gManagerEventsListener.expect(["onCompatibilityModeChanged"]);
+ AddonManager.checkCompatibility = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.checkCompatibility);
+ do_check_false(Services.prefs.getBoolPref(COMPATIBILITY_PREF));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.checkCompatibility = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.checkCompatibility);
+ do_check_false(Services.prefs.getBoolPref(COMPATIBILITY_PREF));
+
+ gManagerEventsListener.expect(["onCompatibilityModeChanged"]);
+ AddonManager.checkCompatibility = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.checkCompatibility);
+ do_check_false(Services.prefs.prefHasUserValue(COMPATIBILITY_PREF));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.checkCompatibility = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.checkCompatibility);
+ do_check_false(Services.prefs.prefHasUserValue(COMPATIBILITY_PREF));
+
+
+ // AddonManager.checkUpdateSecurity
+ gManagerEventsListener.expect(["onCheckUpdateSecurityChanged"]);
+ AddonManager.checkUpdateSecurity = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.checkUpdateSecurity);
+ if (AddonManager.checkUpdateSecurityDefault)
+ do_check_false(Services.prefs.getBoolPref("extensions.checkUpdateSecurity"));
+ else
+ do_check_false(Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.checkUpdateSecurity = false;
+ gManagerEventsListener.checkExpected();
+ do_check_false(AddonManager.checkUpdateSecurity);
+ if (AddonManager.checkUpdateSecurityDefault)
+ do_check_false(Services.prefs.getBoolPref("extensions.checkUpdateSecurity"));
+ else
+ do_check_false(Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity"));
+
+ gManagerEventsListener.expect(["onCheckUpdateSecurityChanged"]);
+ AddonManager.checkUpdateSecurity = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.checkUpdateSecurity);
+ if (!AddonManager.checkUpdateSecurityDefault)
+ do_check_true(Services.prefs.getBoolPref("extensions.checkUpdateSecurity"));
+ else
+ do_check_false(Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity"));
+
+ gManagerEventsListener.expect([]);
+ AddonManager.checkUpdateSecurity = true;
+ gManagerEventsListener.checkExpected();
+ do_check_true(AddonManager.checkUpdateSecurity);
+ if (!AddonManager.checkUpdateSecurityDefault)
+ do_check_true(Services.prefs.getBoolPref("extensions.checkUpdateSecurity"));
+ else
+ do_check_false(Services.prefs.prefHasUserValue("extensions.checkUpdateSecurity"));
+
+ gManagerEventsListener.shutdown();
+
+ // AddonManager.hotfixID
+ let hotfixID = "hotfix@tests.mozilla.org";
+ Services.prefs.setCharPref("extensions.hotfix.id", hotfixID);
+ do_check_eq(AddonManager.hotfixID, hotfixID);
+ // Change the pref and make sure the property is updated
+ hotfixID = "hotfix2@tests.mozilla.org";
+ Services.prefs.setCharPref("extensions.hotfix.id", hotfixID);
+ do_check_eq(AddonManager.hotfixID, hotfixID);
+ // Test an invalid pref value
+ hotfixID = 99;
+ Services.prefs.deleteBranch("extensions.hotfix.id");
+ Services.prefs.setIntPref("extensions.hotfix.id", hotfixID);
+ do_check_eq(AddonManager.hotfixID, null);
+ Services.prefs.clearUserPref("extensions.hotfix.id");
+
+ // After removing the listener, ensure we get no further events.
+ gManagerEventsListener.expect([]);
+ AddonManager.updateEnabled = false;
+ gManagerEventsListener.checkExpected();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js
new file mode 100644
index 000000000..228eb7d34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_markSafe.js
@@ -0,0 +1,49 @@
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+var startupOrder = [];
+
+function mockAddonProvider(name) {
+ let mockProvider = {
+ markSafe: false,
+ apiAccessed: false,
+
+ startup() {
+ if (this.markSafe)
+ AddonManagerPrivate.markProviderSafe(this);
+
+ let uri = Services.io.newURI("beard://long", null, null);
+ AddonManager.isInstallEnabled("made-up-mimetype");
+ },
+ supportsMimetype(mimetype) {
+ this.apiAccessed = true;
+ return false;
+ },
+
+ get name() {
+ return name;
+ },
+ };
+
+ return mockProvider;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* testMarkSafe() {
+ do_print("Starting with provider normally");
+ let provider = mockAddonProvider("Mock1");
+ AddonManagerPrivate.registerProvider(provider);
+ startupManager();
+ ok(!provider.apiAccessed, "Provider API should not have been accessed");
+ AddonManagerPrivate.unregisterProvider(provider);
+ yield promiseShutdownManager();
+
+ do_print("Starting with provider that marks itself safe");
+ provider.apiAccessed = false;
+ provider.markSafe = true;
+ AddonManagerPrivate.registerProvider(provider);
+ startupManager();
+ ok(provider.apiAccessed, "Provider API should have been accessed");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js
new file mode 100644
index 000000000..d210eb81d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_shutdown.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Verify that we report shutdown status for Addon Manager providers
+// and AddonRepository correctly.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+// Make a mock AddonRepository that just lets us hang shutdown.
+// Needs two promises - one to let us know that AM has called shutdown,
+// and one for us to let AM know that shutdown is done.
+function mockAddonProvider(aName) {
+ let mockProvider = {
+ donePromise: null,
+ doneResolve: null,
+ doneReject: null,
+ shutdownPromise: null,
+ shutdownResolve: null,
+
+ get name() {
+ return aName;
+ },
+
+ shutdown() {
+ this.shutdownResolve();
+ return this.donePromise;
+ },
+ };
+ mockProvider.donePromise = new Promise((resolve, reject) => {
+ mockProvider.doneResolve = resolve;
+ mockProvider.doneReject = reject;
+ });
+ mockProvider.shutdownPromise = new Promise((resolve, reject) => {
+ mockProvider.shutdownResolve = resolve;
+ });
+ return mockProvider;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+// Helper to find a particular shutdown blocker's status in the JSON blob
+function findInStatus(aStatus, aName) {
+ for (let {name, state} of aStatus.state) {
+ if (name == aName) {
+ return state;
+ }
+ }
+ return null;
+}
+
+/*
+ * Make sure we report correctly when an add-on provider or AddonRepository block shutdown
+ */
+add_task(function* blockRepoShutdown() {
+ // Reach into the AddonManager scope and inject our mock AddonRepository
+ let realAddonRepo = AMscope.AddonRepository;
+ // the mock provider behaves enough like AddonRepository for the purpose of this test
+ let mockRepo = mockAddonProvider("Mock repo");
+ AMscope.AddonRepository = mockRepo;
+
+ let mockProvider = mockAddonProvider("Mock provider");
+
+ startupManager();
+ AddonManagerPrivate.registerProvider(mockProvider);
+
+ // Start shutting the manager down
+ let managerDown = promiseShutdownManager();
+
+ // Wait for manager to call provider shutdown.
+ yield mockProvider.shutdownPromise;
+ // check AsyncShutdown state
+ let status = MockAsyncShutdown.status();
+ equal(findInStatus(status[0], "Mock provider"), "(none)");
+ equal(status[1].name, "AddonRepository: async shutdown");
+ equal(status[1].state, "pending");
+ // let the provider finish
+ mockProvider.doneResolve();
+
+ // Wait for manager to call repo shutdown and start waiting for it
+ yield mockRepo.shutdownPromise;
+ // Check the shutdown state
+ status = MockAsyncShutdown.status();
+ equal(status[0].name, "AddonManager: Waiting for providers to shut down.");
+ equal(status[0].state, "Complete");
+ equal(status[1].name, "AddonRepository: async shutdown");
+ equal(status[1].state, "in progress");
+
+ // Now finish our shutdown, and wait for the manager to wrap up
+ mockRepo.doneResolve();
+ yield managerDown;
+
+ // Check the shutdown state again
+ status = MockAsyncShutdown.status();
+ equal(status[0].name, "AddonRepository: async shutdown");
+ equal(status[0].state, "done");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js
new file mode 100644
index 000000000..f90e38292
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_shutdown.js
@@ -0,0 +1,64 @@
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+var shutdownOrder = [];
+
+function mockAddonProvider(name) {
+ let mockProvider = {
+ hasShutdown: false,
+ unsafeAccess: false,
+
+ shutdownCallback: null,
+
+ startup() { },
+ shutdown() {
+ this.hasShutdown = true;
+ shutdownOrder.push(this.name);
+ if (this.shutdownCallback)
+ return this.shutdownCallback();
+ return undefined;
+ },
+ getAddonByID(id, callback) {
+ if (this.hasShutdown) {
+ this.unsafeAccess = true;
+ }
+ callback(null);
+ },
+
+ get name() {
+ return name;
+ },
+ };
+
+ return mockProvider;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* unsafeProviderShutdown() {
+ let firstProvider = mockAddonProvider("Mock1");
+ AddonManagerPrivate.registerProvider(firstProvider);
+ let secondProvider = mockAddonProvider("Mock2");
+ AddonManagerPrivate.registerProvider(secondProvider);
+
+ startupManager();
+
+ let shutdownPromise = null;
+ yield new Promise(resolve => {
+ secondProvider.shutdownCallback = function() {
+ return new Promise(shutdownResolve => {
+ AddonManager.getAddonByID("does-not-exist", () => {
+ shutdownResolve();
+ resolve();
+ });
+ });
+ };
+
+ shutdownPromise = promiseShutdownManager();
+ });
+ yield shutdownPromise;
+
+ equal(shutdownOrder.join(","), ["Mock1", "Mock2"].join(","), "Mock providers should have shutdown in expected order");
+ ok(!firstProvider.unsafeAccess, "First registered mock provider should not have been accessed unsafely");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js
new file mode 100644
index 000000000..1193ddfe4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_provider_unsafe_access_startup.js
@@ -0,0 +1,55 @@
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+var startupOrder = [];
+
+function mockAddonProvider(name) {
+ let mockProvider = {
+ hasStarted: false,
+ unsafeAccess: false,
+
+ startupCallback: null,
+
+ startup() {
+ this.hasStarted = true;
+ startupOrder.push(this.name);
+ if (this.startupCallback)
+ this.startupCallback();
+ },
+ getAddonByID(id, callback) {
+ if (!this.hasStarted) {
+ this.unsafeAccess = true;
+ }
+ callback(null);
+ },
+
+ get name() {
+ return name;
+ },
+ };
+
+ return mockProvider;
+}
+
+function run_test() {
+ run_next_test();
+}
+
+add_task(function* unsafeProviderStartup() {
+ let secondProvider = null;
+
+ yield new Promise(resolve => {
+ let firstProvider = mockAddonProvider("Mock1");
+ firstProvider.startupCallback = function() {
+ AddonManager.getAddonByID("does-not-exist", resolve);
+ };
+ AddonManagerPrivate.registerProvider(firstProvider);
+
+ secondProvider = mockAddonProvider("Mock2");
+ AddonManagerPrivate.registerProvider(secondProvider);
+
+ startupManager();
+ });
+
+ equal(startupOrder.join(","), ["Mock1", "Mock2"].join(","), "Mock providers should have hasStarted in expected order");
+ ok(!secondProvider.unsafeAccess, "Second registered mock provider should not have been accessed unsafely");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js b/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js
new file mode 100644
index 000000000..7b28c78f2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_proxies.js
@@ -0,0 +1,240 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests the semantics of extension proxy files and symlinks
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+var ADDONS = [
+ {
+ id: "proxy1@tests.mozilla.org",
+ dirId: "proxy1@tests.mozilla.com",
+ type: "proxy"
+ },
+ {
+ id: "proxy2@tests.mozilla.org",
+ type: "proxy"
+ },
+ {
+ id: "symlink1@tests.mozilla.org",
+ dirId: "symlink1@tests.mozilla.com",
+ type: "symlink"
+ },
+ {
+ id: "symlink2@tests.mozilla.org",
+ type: "symlink"
+ }
+];
+
+var METADATA = {
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+}
+
+const ios = AM_Cc["@mozilla.org/network/io-service;1"].getService(AM_Ci.nsIIOService);
+
+const LocalFile = Components.Constructor("@mozilla.org/file/local;1",
+ "nsILocalFile", "initWithPath");
+const Process = Components.Constructor("@mozilla.org/process/util;1",
+ "nsIProcess", "init");
+
+const gHaveSymlinks = AppConstants.platform != "win";
+
+
+function createSymlink(aSource, aDest) {
+ if (aSource instanceof AM_Ci.nsIFile)
+ aSource = aSource.path;
+ if (aDest instanceof AM_Ci.nsIFile)
+ aDest = aDest.path;
+
+ return OS.File.unixSymLink(aSource, aDest);
+}
+
+function writeFile(aData, aFile) {
+ if (!aFile.parent.exists())
+ aFile.parent.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+ var fos = AM_Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(AM_Ci.nsIFileOutputStream);
+ fos.init(aFile,
+ FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE | FileUtils.MODE_TRUNCATE,
+ FileUtils.PERMS_FILE, 0);
+ fos.write(aData, aData.length);
+ fos.close();
+}
+
+function checkAddonsExist() {
+ for (let addon of ADDONS) {
+ let file = addon.directory.clone();
+ file.append("install.rdf");
+ do_check_true(file.exists(), Components.stack.caller);
+ }
+}
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2", "2");
+
+ add_task(run_proxy_tests);
+
+ if (gHaveSymlinks)
+ add_task(run_symlink_tests);
+
+ run_next_test();
+}
+
+function* run_proxy_tests() {
+ if (!gHaveSymlinks) {
+ ADDONS = ADDONS.filter(a => a.type != "symlink");
+ }
+
+ for (let addon of ADDONS) {
+ addon.directory = gTmpD.clone();
+ addon.directory.append(addon.id);
+
+ addon.proxyFile = profileDir.clone();
+ addon.proxyFile.append(addon.dirId || addon.id);
+
+ METADATA.id = addon.id;
+ METADATA.name = addon.id;
+ writeInstallRDFToDir(METADATA, gTmpD);
+
+ if (addon.type == "proxy") {
+ writeFile(addon.directory.path, addon.proxyFile)
+ }
+ else if (addon.type == "symlink") {
+ yield createSymlink(addon.directory, addon.proxyFile)
+ }
+ }
+
+ startupManager();
+
+ // Check that all add-ons original sources still exist after invalid
+ // add-ons have been removed at startup.
+ checkAddonsExist();
+
+ return new Promise(resolve => {
+ AddonManager.getAddonsByIDs(ADDONS.map(addon => addon.id), resolve);
+ }).then(addons => {
+ try {
+ for (let [i, addon] of addons.entries()) {
+ // Ensure that valid proxied add-ons were installed properly on
+ // platforms that support the installation method.
+ print(ADDONS[i].id,
+ ADDONS[i].dirId,
+ ADDONS[i].dirId != null,
+ ADDONS[i].type == "symlink");
+ do_check_eq(addon == null,
+ ADDONS[i].dirId != null);
+
+ if (addon != null) {
+ let fixURL = url => {
+ if (AppConstants.platform == "macosx")
+ return url.replace(RegExp(`^file:///private/`), "file:///");
+ return url;
+ };
+
+ // Check that proxied add-ons do not have upgrade permissions.
+ do_check_eq(addon.permissions & AddonManager.PERM_CAN_UPGRADE, 0);
+
+ // Check that getResourceURI points to the right place.
+ do_check_eq(ios.newFileURI(ADDONS[i].directory).spec,
+ fixURL(addon.getResourceURI().spec),
+ `Base resource URL resolves as expected`);
+
+ let file = ADDONS[i].directory.clone();
+ file.append("install.rdf");
+
+ do_check_eq(ios.newFileURI(file).spec,
+ fixURL(addon.getResourceURI("install.rdf").spec),
+ `Resource URLs resolve as expected`);
+
+ addon.uninstall();
+ }
+ }
+
+ // Check that original sources still exist after explicit uninstall.
+ restartManager();
+ checkAddonsExist();
+
+ shutdownManager();
+
+ // Check that all of the proxy files have been removed and remove
+ // the original targets.
+ for (let addon of ADDONS) {
+ equal(addon.proxyFile.exists(), addon.dirId != null,
+ `Proxy file ${addon.proxyFile.path} should exist?`);
+ addon.directory.remove(true);
+ try {
+ addon.proxyFile.remove(false);
+ } catch (e) {}
+ }
+ }
+ catch (e) {
+ do_throw(e);
+ }
+ });
+}
+
+function* run_symlink_tests() {
+ // Check that symlinks are not followed out of a directory tree
+ // when deleting an add-on.
+
+ METADATA.id = "unpacked@test.mozilla.org";
+ METADATA.name = METADATA.id;
+ METADATA.unpack = "true";
+
+ let tempDirectory = gTmpD.clone();
+ tempDirectory.append(METADATA.id);
+
+ let tempFile = tempDirectory.clone();
+ tempFile.append("test.txt");
+ tempFile.create(AM_Ci.nsIFile.NORMAL_FILE_TYPE, 0o644);
+
+ let addonDirectory = profileDir.clone();
+ addonDirectory.append(METADATA.id);
+
+ writeInstallRDFToDir(METADATA, profileDir);
+
+ let symlink = addonDirectory.clone();
+ symlink.append(tempDirectory.leafName);
+ yield createSymlink(tempDirectory, symlink);
+
+ // Make sure that the symlink was created properly.
+ let file = symlink.clone();
+ file.append(tempFile.leafName);
+ file.normalize();
+ do_check_eq(file.path.replace(/^\/private\//, "/"), tempFile.path);
+
+ startupManager();
+
+ return new Promise(resolve => {
+ AddonManager.getAddonByID(METADATA.id, resolve);
+ }).then(addon => {
+ do_check_neq(addon, null);
+
+ addon.uninstall();
+
+ restartManager();
+ shutdownManager();
+
+ // Check that the install directory is gone.
+ do_check_false(addonDirectory.exists());
+
+ // Check that the temp file is not gone.
+ do_check_true(tempFile.exists());
+
+ tempDirectory.remove(true);
+ });
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_proxy.js b/toolkit/mozapps/extensions/test/xpcshell/test_proxy.js
new file mode 100644
index 000000000..c35870c9b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_proxy.js
@@ -0,0 +1,106 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "proxy1@tests.mozilla.org";
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+BootstrapMonitor.init();
+
+// Ensure that a proxy file to an add-on with a valid manifest works.
+add_task(function*() {
+ let tempdir = gTmpD.clone();
+ writeInstallRDFToDir({
+ id: ID,
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (proxy)",
+ }, tempdir, ID, "bootstrap.js");
+
+ let unpackedAddon = tempdir.clone();
+ unpackedAddon.append(ID);
+ do_get_file("data/test_proxy/bootstrap.js")
+ .copyTo(unpackedAddon, "bootstrap.js");
+
+ // create proxy file in profile/extensions dir
+ let extensionsDir = gProfD.clone();
+ extensionsDir.append("extensions");
+ let proxyFile = writeProxyFileToDir(extensionsDir, unpackedAddon, ID);
+
+ yield promiseRestartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1 (proxy)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ do_check_true(proxyFile.exists());
+
+ addon.uninstall();
+ unpackedAddon.remove(true);
+
+ yield promiseRestartManager();
+});
+
+
+// Ensure that a proxy file to an add-on is not removed even
+// if the manifest file is invalid. See bug 1195353.
+add_task(function*() {
+ let tempdir = gTmpD.clone();
+
+ // use a mismatched ID to make this install.rdf invalid
+ writeInstallRDFToDir({
+ id: "bad-proxy1@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (proxy)",
+ }, tempdir, ID, "bootstrap.js");
+
+ let unpackedAddon = tempdir.clone();
+ unpackedAddon.append(ID);
+ do_get_file("data/test_proxy/bootstrap.js")
+ .copyTo(unpackedAddon, "bootstrap.js");
+
+ // create proxy file in profile/extensions dir
+ let extensionsDir = gProfD.clone();
+ extensionsDir.append("extensions");
+ let proxyFile = writeProxyFileToDir(extensionsDir, unpackedAddon, ID);
+
+ yield promiseRestartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ do_check_true(proxyFile.exists());
+
+ unpackedAddon.remove(true);
+ proxyFile.remove(true);
+
+ yield promiseRestartManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_registry.js b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js
new file mode 100644
index 000000000..eab7af6b6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_registry.js
@@ -0,0 +1,158 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Tests that extensions installed through the registry work as expected
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +
+ AddonManager.SCOPE_SYSTEM);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+const addon1Dir = writeInstallRDFForExtension(addon1, gProfD, "addon1");
+const addon2Dir = writeInstallRDFForExtension(addon2, gProfD, "addon2");
+
+let registry;
+
+function run_test() {
+ // This test only works where there is a registry.
+ if (!("nsIWindowsRegKey" in AM_Ci))
+ return;
+
+ registry = new MockRegistry();
+ do_register_cleanup(() => {
+ registry.shutdown();
+ });
+
+ do_test_pending();
+
+ run_test_1();
+}
+
+// Tests whether basic registry install works
+function run_test_1() {
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", addon1Dir.path);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", addon2Dir.path);
+
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a1.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Tests whether uninstalling from the registry works
+function run_test_2() {
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", null);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", null);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_eq(a1, null);
+ do_check_eq(a2, null);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Checks that the ID in the registry must match that in the install manifest
+function run_test_3() {
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", addon2Dir.path);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", addon1Dir.path);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_eq(a1, null);
+ do_check_eq(a2, null);
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Tests whether an extension's ID can change without its directory changing
+function run_test_4() {
+ // Restarting with bad items in the registry should not force an EM restart
+ restartManager();
+
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", null);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", null);
+
+ restartManager();
+
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", addon1Dir.path);
+ restartManager();
+
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon1@tests.mozilla.org", null);
+ registry.setValue(AM_Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ "SOFTWARE\\Mozilla\\XPCShell\\Extensions",
+ "addon2@tests.mozilla.org", addon1Dir.path);
+ writeInstallRDFForExtension(addon2, gProfD, "addon1");
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"], function([a1, a2]) {
+ do_check_eq(a1, null);
+ do_check_neq(a2, null);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_reload.js b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
new file mode 100644
index 000000000..5873d1980
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_reload.js
@@ -0,0 +1,235 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+const sampleAddon = {
+ id: "webextension1@tests.mozilla.org",
+ name: "webextension_1",
+}
+
+const manifestSample = {
+ id: "bootstrap1@tests.mozilla.org",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+};
+
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseAddonStartup() {
+ return new Promise(resolve => {
+ let listener = (extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+function* installAddon(fixtureName, addonID) {
+ yield promiseInstallAllFiles([do_get_addon(fixtureName)]);
+ return promiseAddonByID(addonID);
+}
+
+function* tearDownAddon(addon) {
+ addon.uninstall();
+ yield promiseShutdownManager();
+}
+
+add_task(function* test_reloading_a_temp_addon() {
+ yield promiseRestartManager();
+ yield AddonManager.installTemporaryAddon(do_get_addon(sampleAddon.name));
+ const addon = yield promiseAddonByID(sampleAddon.id)
+
+ var receivedOnUninstalled = false;
+ var receivedOnUninstalling = false;
+ var receivedOnInstalled = false;
+ var receivedOnInstalling = false;
+
+ const onReload = new Promise(resolve => {
+ const listener = {
+ onUninstalling: (addonObj) => {
+ if (addonObj.id === sampleAddon.id) {
+ receivedOnUninstalling = true;
+ }
+ },
+ onUninstalled: (addonObj) => {
+ if (addonObj.id === sampleAddon.id) {
+ receivedOnUninstalled = true;
+ }
+ },
+ onInstalling: (addonObj) => {
+ receivedOnInstalling = true;
+ equal(addonObj.id, sampleAddon.id);
+ },
+ onInstalled: (addonObj) => {
+ receivedOnInstalled = true;
+ equal(addonObj.id, sampleAddon.id);
+ // This should be the last event called.
+ AddonManager.removeAddonListener(listener);
+ resolve();
+ },
+ }
+ AddonManager.addAddonListener(listener);
+ });
+
+ yield addon.reload();
+ yield onReload;
+
+ // Make sure reload() doesn't trigger uninstall events.
+ equal(receivedOnUninstalled, false, "reload should not trigger onUninstalled");
+ equal(receivedOnUninstalling, false, "reload should not trigger onUninstalling");
+
+ // Make sure reload() triggers install events, like an upgrade.
+ equal(receivedOnInstalling, true, "reload should trigger onInstalling");
+ equal(receivedOnInstalled, true, "reload should trigger onInstalled");
+
+ yield tearDownAddon(addon);
+});
+
+add_task(function* test_can_reload_permanent_addon() {
+ yield promiseRestartManager();
+ const addon = yield installAddon(sampleAddon.name, sampleAddon.id);
+
+ let disabledCalled = false;
+ let enabledCalled = false;
+ AddonManager.addAddonListener({
+ onDisabled: (aAddon) => {
+ do_check_false(enabledCalled);
+ disabledCalled = true
+ },
+ onEnabled: (aAddon) => {
+ do_check_true(disabledCalled);
+ enabledCalled = true
+ }
+ })
+
+ yield addon.reload();
+
+ do_check_true(disabledCalled);
+ do_check_true(enabledCalled);
+
+ notEqual(addon, null);
+ equal(addon.appDisabled, false);
+ equal(addon.userDisabled, false);
+
+ yield tearDownAddon(addon);
+});
+
+add_task(function* test_reload_to_invalid_version_fails() {
+ yield promiseRestartManager();
+ let tempdir = gTmpD.clone();
+
+ // The initial version of the add-on will be compatible, and will therefore load
+ const addonId = "invalid_version_cannot_be_reloaded@tests.mozilla.org";
+ let manifest = {
+ name: "invalid_version_cannot_be_reloaded",
+ description: "test invalid_version_cannot_be_reloaded",
+ manifest_version: 2,
+ version: "1.0",
+ applications: {
+ gecko: {
+ id: addonId,
+ }
+ },
+ };
+
+ let addonDir = yield promiseWriteWebManifestForExtension(manifest, tempdir, "invalid_version");
+ yield AddonManager.installTemporaryAddon(addonDir);
+ yield promiseAddonStartup();
+
+ let addon = yield promiseAddonByID(addonId);
+ notEqual(addon, null);
+ equal(addon.id, addonId);
+ equal(addon.version, "1.0");
+ equal(addon.appDisabled, false);
+ equal(addon.userDisabled, false);
+ addonDir.remove(true);
+
+ // update the manifest to make the add-on version incompatible, so the reload will reject
+ manifest.applications.gecko.strict_min_version = "1";
+ manifest.applications.gecko.strict_max_version = "1";
+ manifest.version = "2.0";
+
+ addonDir = yield promiseWriteWebManifestForExtension(manifest, tempdir, "invalid_version", false);
+ let expectedMsg = new RegExp("Add-on invalid_version_cannot_be_reloaded@tests.mozilla.org is not compatible with application version. " +
+ "add-on minVersion: 1. add-on maxVersion: 1.");
+
+ yield Assert.rejects(addon.reload(),
+ expectedMsg,
+ "Reload rejects when application version does not fall between minVersion and maxVersion");
+
+ let reloadedAddon = yield promiseAddonByID(addonId);
+ notEqual(reloadedAddon, null);
+ equal(reloadedAddon.id, addonId);
+ equal(reloadedAddon.version, "1.0");
+ equal(reloadedAddon.appDisabled, false);
+ equal(reloadedAddon.userDisabled, false);
+
+ yield tearDownAddon(reloadedAddon);
+ addonDir.remove(true);
+});
+
+add_task(function* test_manifest_changes_are_refreshed() {
+ yield promiseRestartManager();
+ let tempdir = gTmpD.clone();
+
+ const unpackedAddon = writeInstallRDFToDir(
+ Object.assign({}, manifestSample, {
+ name: "Test Bootstrap 1",
+ }), tempdir, manifestSample.id, "bootstrap.js");
+
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+ const addon = yield promiseAddonByID(manifestSample.id);
+ notEqual(addon, null);
+ equal(addon.name, "Test Bootstrap 1");
+
+ writeInstallRDFToDir(Object.assign({}, manifestSample, {
+ name: "Test Bootstrap 1 (reloaded)",
+ }), tempdir, manifestSample.id);
+
+ yield addon.reload();
+
+ const reloadedAddon = yield promiseAddonByID(manifestSample.id);
+ notEqual(reloadedAddon, null);
+ equal(reloadedAddon.name, "Test Bootstrap 1 (reloaded)");
+
+ yield tearDownAddon(reloadedAddon);
+ unpackedAddon.remove(true);
+});
+
+add_task(function* test_reload_fails_on_installation_errors() {
+ yield promiseRestartManager();
+ let tempdir = gTmpD.clone();
+
+ const unpackedAddon = writeInstallRDFToDir(
+ Object.assign({}, manifestSample, {
+ name: "Test Bootstrap 1",
+ }), tempdir, manifestSample.id, "bootstrap.js");
+
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+ const addon = yield promiseAddonByID(manifestSample.id);
+ notEqual(addon, null);
+
+ // Trigger an installation error with an empty manifest.
+ writeInstallRDFToDir({}, tempdir, manifestSample.id);
+
+ yield Assert.rejects(addon.reload(), /No ID in install manifest/);
+
+ // The old add-on should be active. I.E. the broken reload will not
+ // disturb it.
+ const oldAddon = yield promiseAddonByID(manifestSample.id);
+ notEqual(oldAddon, null);
+ equal(oldAddon.isActive, true);
+ equal(oldAddon.name, "Test Bootstrap 1");
+
+ yield tearDownAddon(addon);
+ unpackedAddon.remove(true);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js b/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
new file mode 100644
index 000000000..05647f807
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_safemode.js
@@ -0,0 +1,115 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+// Tests that extensions behave correctly in safe mode
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ optionsURL: "chrome://foo/content/options.xul",
+ aboutURL: "chrome://foo/content/about.xul",
+ iconURL: "chrome://foo/content/icon.png",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gIconURL = null;
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ gAppInfo.inSafeMode = true;
+
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(a1) {
+ do_check_eq(a1, null);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ writeInstallRDFForExtension(addon1, profileDir, addon1.id, "icon.png");
+ gIconURL = do_get_addon_root_uri(profileDir.clone(), addon1.id) + "icon.png";
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(newa1) {
+ do_check_neq(newa1, null);
+ do_check_false(newa1.isActive);
+ do_check_false(newa1.userDisabled);
+ do_check_eq(newa1.aboutURL, null);
+ do_check_eq(newa1.optionsURL, null);
+ do_check_eq(newa1.iconURL, gIconURL);
+ do_check_true(isExtensionInAddonsList(profileDir, newa1.id));
+ do_check_true(hasFlag(newa1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(newa1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(newa1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_NONE);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ run_test_1();
+ });
+ }));
+}
+
+// Disabling an add-on should work
+function run_test_1() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_false(hasFlag(a1.operationsRequiringRestart,
+ AddonManager.OP_NEEDS_RESTART_DISABLE));
+ a1.userDisabled = true;
+ do_check_false(a1.isActive);
+ do_check_eq(a1.aboutURL, null);
+ do_check_eq(a1.optionsURL, null);
+ do_check_eq(a1.iconURL, gIconURL);
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(a1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_NONE);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ ensure_test_completed();
+
+ run_test_2();
+ });
+}
+
+// Enabling an add-on should happen without restart but not become active.
+function run_test_2() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ a1.userDisabled = false;
+ do_check_false(a1.isActive);
+ do_check_eq(a1.aboutURL, null);
+ do_check_eq(a1.optionsURL, null);
+ do_check_eq(a1.iconURL, gIconURL);
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(a1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_NONE);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ ensure_test_completed();
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js b/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js
new file mode 100644
index 000000000..3d386f663
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_schema_change.js
@@ -0,0 +1,317 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+BootstrapMonitor.init();
+
+const PREF_DB_SCHEMA = "extensions.databaseSchema";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+startupManager();
+
+/**
+ * Schema change with no application update reloads metadata.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallAllFiles([xpiFile]);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield shutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, 0);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * Application update with no schema change does not reload metadata.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ });
+
+ yield promiseInstallAllFiles([xpiFile]);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield shutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ });
+
+ gAppInfo.version = "2";
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * App update and a schema change causes a reload of the manifest.
+ */
+add_task(function* schema_change_app_update() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ });
+
+ yield promiseInstallAllFiles([xpiFile]);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield shutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "3"
+ }]
+ });
+
+ gAppInfo.version = "3";
+ Services.prefs.setIntPref(PREF_DB_SCHEMA, 0);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.appDisabled, false);
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * No schema change, no manifest reload.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallAllFiles([xpiFile]);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield shutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Make sure the timestamp is unchanged, so it is not re-scanned for that reason.
+ let timestamp = file.lastModifiedTime;
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * Modified timestamp on the XPI causes a reload of the manifest.
+ */
+add_task(function* schema_change() {
+ const ID = "schema-change@tests.mozilla.org";
+
+ let xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ yield promiseInstallAllFiles([xpiFile]);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+
+ yield shutdownManager();
+
+ xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on 2",
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ });
+
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ let file = profileDir.clone();
+ file.append(`${ID}.xpi`);
+
+ // Set timestamp in the future so manifest is re-scanned.
+ let timestamp = new Date(Date.now() + 60000);
+ xpiFile.moveTo(profileDir, `${ID}.xpi`);
+
+ file.lastModifiedTime = timestamp;
+
+ yield startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "2.0", "Got the expected version");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_seen.js b/toolkit/mozapps/extensions/test/xpcshell/test_seen.js
new file mode 100644
index 000000000..e499e7339
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_seen.js
@@ -0,0 +1,211 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+
+let profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+startupManager();
+
+// By default disable add-ons from the profile
+Services.prefs.setIntPref("extensions.autoDisableScopes", AddonManager.SCOPE_PROFILE);
+
+// Installing an add-on through the API should mark it as seen
+add_task(function*() {
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ let addon = install.addon;
+ do_check_eq(addon.version, "1.0");
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ // Installing an update should retain that
+ install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ addon = install.addon;
+ do_check_eq(addon.version, "2.0");
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+ let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+ // Make sure the startup code will detect sideloaded updates
+ setExtensionModifiedTime(path, Date.now() - 10000);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "1.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ yield promiseShutdownManager();
+
+ // Sideloading an update shouldn't change the state
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir, ID);
+ setExtensionModifiedTime(path, Date.now());
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "2.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+ let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+ // Make sure the startup code will detect sideloaded updates
+ setExtensionModifiedTime(path, Date.now() - 10000);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "1.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ // Updating through the API shouldn't change the state
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ addon = install.addon;
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "2.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+ let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+ // Make sure the startup code will detect sideloaded updates
+ setExtensionModifiedTime(path, Date.now() - 10000);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "1.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+ addon.markAsSeen();
+ do_check_true(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ yield promiseShutdownManager();
+
+ // Sideloading an update shouldn't change the state
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_addon("test_bootstrap1_2"), profileDir, ID);
+ setExtensionModifiedTime(path, Date.now());
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "2.0");
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+});
+
+// Sideloading an add-on should mark it as unseen
+add_task(function*() {
+ let path = manuallyInstall(do_get_addon("test_bootstrap1_1"), profileDir, ID);
+ // Make sure the startup code will detect sideloaded updates
+ setExtensionModifiedTime(path, Date.now() - 10000);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "1.0");
+ do_check_true(addon.foreignInstall);
+ do_check_false(addon.seen);
+ addon.markAsSeen();
+ do_check_true(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ // Updating through the API shouldn't change the state
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_2"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_false(hasFlag(install.addon.pendingOperations, AddonManager.PENDING_INSTALL));
+
+ addon = install.addon;
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_eq(addon.version, "2.0");
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_seen_newprofile.js b/toolkit/mozapps/extensions/test/xpcshell/test_seen_newprofile.js
new file mode 100644
index 000000000..43ee18594
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_seen_newprofile.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_SYSTEM);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+BootstrapMonitor.init();
+
+const globalDir = gProfD.clone();
+globalDir.append("extensions2");
+globalDir.append(gAppInfo.ID);
+registerDirectory("XRESysSExtPD", globalDir.parent);
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// By default disable add-ons from the system
+Services.prefs.setIntPref("extensions.autoDisableScopes", AddonManager.SCOPE_SYSTEM);
+
+// When new add-ons already exist in a system location when starting with a new
+// profile they should be marked as already seen.
+add_task(function*() {
+ manuallyInstall(do_get_addon("test_bootstrap1_1"), globalDir, ID);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_true(addon.foreignInstall);
+ do_check_true(addon.seen);
+ do_check_true(addon.userDisabled);
+ do_check_false(addon.isActive);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
new file mode 100644
index 000000000..725887bc1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_shutdown.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Verify that API functions fail if the Add-ons Manager isn't initialised.
+
+const IGNORE = ["getPreferredIconURL", "escapeAddonURI",
+ "shouldAutoUpdate", "getStartupChanges",
+ "addTypeListener", "removeTypeListener",
+ "addAddonListener", "removeAddonListener",
+ "addInstallListener", "removeInstallListener",
+ "addManagerListener", "removeManagerListener",
+ "mapURIToAddonID", "shutdown", "init",
+ "stateToString", "errorToString", "getUpgradeListener",
+ "addUpgradeListener", "removeUpgradeListener"];
+
+const IGNORE_PRIVATE = ["AddonAuthor", "AddonCompatibilityOverride",
+ "AddonScreenshot", "AddonType", "startup", "shutdown",
+ "registerProvider", "unregisterProvider",
+ "addStartupChange", "removeStartupChange",
+ "recordTimestamp", "recordSimpleMeasure",
+ "recordException", "getSimpleMeasures", "simpleTimer",
+ "setTelemetryDetails", "getTelemetryDetails",
+ "callNoUpdateListeners", "backgroundUpdateTimerHandler",
+ "hasUpgradeListener", "getUpgradeListener"];
+
+function test_functions() {
+ for (let prop in AddonManager) {
+ if (IGNORE.indexOf(prop) != -1)
+ continue;
+ if (typeof AddonManager[prop] != "function")
+ continue;
+
+ let args = [];
+
+ // Getter functions need a callback and in some cases not having one will
+ // throw before checking if the add-ons manager is initialized so pass in
+ // an empty one.
+ if (prop.startsWith("get")) {
+ // For now all getter functions with more than one argument take the
+ // callback in the second argument.
+ if (AddonManager[prop].length > 1) {
+ args.push(undefined, () => {});
+ }
+ else {
+ args.push(() => {});
+ }
+ }
+
+ try {
+ do_print("AddonManager." + prop);
+ AddonManager[prop](...args);
+ do_throw(prop + " did not throw an exception");
+ }
+ catch (e) {
+ if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
+ do_throw(prop + " threw an unexpected exception: " + e);
+ }
+ }
+
+ for (let prop in AddonManagerPrivate) {
+ if (typeof AddonManagerPrivate[prop] != "function")
+ continue;
+ if (IGNORE_PRIVATE.indexOf(prop) != -1)
+ continue;
+
+ try {
+ do_print("AddonManagerPrivate." + prop);
+ AddonManagerPrivate[prop]();
+ do_throw(prop + " did not throw an exception");
+ }
+ catch (e) {
+ if (e.result != Components.results.NS_ERROR_NOT_INITIALIZED)
+ do_throw(prop + " threw an unexpected exception: " + e);
+ }
+ }
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ test_functions();
+ startupManager();
+ shutdownManager();
+ test_functions();
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
new file mode 100644
index 000000000..3b96f40ba
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_inject.js
@@ -0,0 +1,382 @@
+// Enable signature checks for these tests
+gUseRealCertChecks = true;
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+ bootstrap: {
+ unsigned: "unsigned_bootstrap_2.xpi",
+ badid: "signed_bootstrap_badid_2.xpi",
+ signed: "signed_bootstrap_2.xpi",
+ preliminary: "preliminary_bootstrap_2.xpi",
+ },
+ nonbootstrap: {
+ unsigned: "unsigned_nonbootstrap_2.xpi",
+ badid: "signed_nonbootstrap_badid_2.xpi",
+ signed: "signed_nonbootstrap_2.xpi",
+ }
+};
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Deletes a file from the test add-on in the profile
+function breakAddon(file) {
+ if (TEST_UNPACKED) {
+ file.append("test.txt");
+ file.remove(true);
+ }
+ else {
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+ createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(file, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+ zipW.removeEntry("test.txt", false);
+ zipW.close();
+ }
+}
+
+function resetPrefs() {
+ Services.prefs.setIntPref("bootstraptest.active_version", -1);
+ Services.prefs.setIntPref("bootstraptest.installed_version", -1);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.install_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1);
+ Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1);
+ Services.prefs.setIntPref("bootstraptest.install_oldversion", -1);
+ Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1);
+}
+
+function clearCache(file) {
+ if (TEST_UNPACKED)
+ return;
+
+ Services.obs.notifyObservers(file, "flush-cache-entry", null);
+}
+
+function getActiveVersion() {
+ return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+ // Start and stop the manager to initialise everything in the profile before
+ // actual testing
+ startupManager();
+ shutdownManager();
+ resetPrefs();
+
+ run_next_test();
+}
+
+// Injecting into profile (bootstrap)
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.unsigned), profileDir, ID);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+ do_check_eq(getActiveVersion(), -1);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID);
+ breakAddon(file);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_eq(getActiveVersion(), -1);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.badid), profileDir, ID);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_eq(getActiveVersion(), -1);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+// Installs a signed add-on then modifies it in place breaking its signing
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), profileDir, ID);
+
+ // Make it appear to come from the past so when we modify it later it is
+ // detected during startup. Obviously malware can bypass this method of
+ // detection but the periodic scan will catch that
+ yield promiseSetExtensionModifiedTime(file.path, Date.now() - 600000);
+
+ startupManager();
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
+ do_check_eq(getActiveVersion(), 2);
+
+ yield promiseShutdownManager();
+ do_check_eq(getActiveVersion(), 0);
+
+ clearCache(file);
+ breakAddon(file);
+ resetPrefs();
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_eq(getActiveVersion(), -1);
+
+ let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ do_check_eq(ids.length, 1);
+ do_check_eq(ids[0], ID);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+// Injecting into profile (non-bootstrap)
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.unsigned), profileDir, ID);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+ do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+ addon.uninstall();
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID);
+ breakAddon(file);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+ addon.uninstall();
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.badid), profileDir, ID);
+
+ startupManager();
+
+ // Currently we leave the sideloaded add-on there but just don't run it
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+ addon.uninstall();
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+// Installs a signed add-on then modifies it in place breaking its signing
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.nonbootstrap.signed), profileDir, ID);
+
+ // Make it appear to come from the past so when we modify it later it is
+ // detected during startup. Obviously malware can bypass this method of
+ // detection but the periodic scan will catch that
+ yield promiseSetExtensionModifiedTime(file.path, Date.now() - 60000);
+
+ startupManager();
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SIGNED);
+ do_check_true(isExtensionInAddonsList(profileDir, ID));
+
+ yield promiseShutdownManager();
+
+ clearCache(file);
+ breakAddon(file);
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_BROKEN);
+ do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+ let ids = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ do_check_eq(ids.length, 1);
+ do_check_eq(ids[0], ID);
+
+ addon.uninstall();
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+// Stage install then modify before startup (non-bootstrap)
+add_task(function*() {
+ startupManager();
+ yield promiseInstallAllFiles([do_get_file(DATA + ADDONS.nonbootstrap.signed)]);
+ yield promiseShutdownManager();
+
+ let staged = profileDir.clone();
+ staged.append("staged");
+ staged.append(do_get_expected_addon_name(ID));
+ do_check_true(staged.exists());
+
+ breakAddon(staged);
+ startupManager();
+
+ // Should have refused to install the broken staged version
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ clearCache(staged);
+
+ yield promiseShutdownManager();
+});
+
+// Manufacture staged install (bootstrap)
+add_task(function*() {
+ let stage = profileDir.clone();
+ stage.append("staged");
+
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.signed), stage, ID);
+ breakAddon(file);
+
+ startupManager();
+
+ // Should have refused to install the broken staged version
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+ do_check_eq(getActiveVersion(), -1);
+
+ do_check_false(file.exists());
+ clearCache(file);
+
+ yield promiseShutdownManager();
+ resetPrefs();
+});
+
+// Preliminarily-signed sideloaded add-ons should work
+add_task(function*() {
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.preliminary), profileDir, ID);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_PRELIMINARY);
+ do_check_eq(getActiveVersion(), 2);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
+
+// Preliminarily-signed sideloaded add-ons should work via staged install
+add_task(function*() {
+ let stage = profileDir.clone();
+ stage.append("staged");
+
+ let file = manuallyInstall(do_get_file(DATA + ADDONS.bootstrap.preliminary), stage, ID);
+
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_PRELIMINARY);
+ do_check_eq(getActiveVersion(), 2);
+
+ addon.uninstall();
+ yield promiseShutdownManager();
+ resetPrefs();
+
+ do_check_false(file.exists());
+ clearCache(file);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js
new file mode 100644
index 000000000..19b07ac16
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_install.js
@@ -0,0 +1,265 @@
+// Enable signature checks for these tests
+gUseRealCertChecks = true;
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+ bootstrap: {
+ unsigned: "unsigned_bootstrap_2.xpi",
+ badid: "signed_bootstrap_badid_2.xpi",
+ preliminary: "preliminary_bootstrap_2.xpi",
+ signed: "signed_bootstrap_2.xpi",
+ },
+};
+const WORKING = "signed_bootstrap_1.xpi";
+const ID = "test@tests.mozilla.org";
+
+var gServer = createHttpServer(4444);
+
+// Creates an add-on with a broken signature by changing an existing file
+function createBrokenAddonModify(file) {
+ let brokenFile = gTmpD.clone();
+ brokenFile.append("broken.xpi");
+ file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+ var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(AM_Ci.nsIStringInputStream);
+ stream.setData("FOOBAR", -1);
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+ createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+ zipW.removeEntry("test.txt", false);
+ zipW.addEntryStream("test.txt", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ zipW.close();
+
+ return brokenFile;
+}
+
+// Creates an add-on with a broken signature by adding a new file
+function createBrokenAddonAdd(file) {
+ let brokenFile = gTmpD.clone();
+ brokenFile.append("broken.xpi");
+ file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+ var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(AM_Ci.nsIStringInputStream);
+ stream.setData("FOOBAR", -1);
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+ createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+ zipW.addEntryStream("test2.txt", 0, AM_Ci.nsIZipWriter.COMPRESSION_NONE,
+ stream, false);
+ zipW.close();
+
+ return brokenFile;
+}
+
+// Creates an add-on with a broken signature by removing an existing file
+function createBrokenAddonRemove(file) {
+ let brokenFile = gTmpD.clone();
+ brokenFile.append("broken.xpi");
+ file.copyTo(brokenFile.parent, brokenFile.leafName);
+
+ var stream = AM_Cc["@mozilla.org/io/string-input-stream;1"].
+ createInstance(AM_Ci.nsIStringInputStream);
+ stream.setData("FOOBAR", -1);
+ var zipW = AM_Cc["@mozilla.org/zipwriter;1"].
+ createInstance(AM_Ci.nsIZipWriter);
+ zipW.open(brokenFile, FileUtils.MODE_RDWR | FileUtils.MODE_APPEND);
+ zipW.removeEntry("test.txt", false);
+ zipW.close();
+
+ return brokenFile;
+}
+
+function createInstall(url) {
+ return new Promise(resolve => {
+ AddonManager.getInstallForURL(url, resolve, "application/x-xpinstall");
+ });
+}
+
+function serveUpdateRDF(leafName) {
+ gServer.registerPathHandler("/update.rdf", function(request, response) {
+ let updateData = {};
+ updateData[ID] = [{
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "4",
+ maxVersion: "6",
+ updateLink: "http://localhost:4444/" + leafName
+ }]
+ }];
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(createUpdateRDF(updateData));
+ });
+}
+
+
+function* test_install_broken(file, expectedError) {
+ gServer.registerFile("/" + file.leafName, file);
+
+ let install = yield createInstall("http://localhost:4444/" + file.leafName);
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, expectedError);
+ do_check_eq(install.addon, null);
+
+ gServer.registerFile("/" + file.leafName, null);
+}
+
+function* test_install_working(file, expectedSignedState) {
+ gServer.registerFile("/" + file.leafName, file);
+
+ let install = yield createInstall("http://localhost:4444/" + file.leafName);
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_neq(install.addon, null);
+ do_check_eq(install.addon.signedState, expectedSignedState);
+
+ gServer.registerFile("/" + file.leafName, null);
+
+ install.addon.uninstall();
+}
+
+function* test_update_broken(file, expectedError) {
+ // First install the older version
+ yield promiseInstallAllFiles([do_get_file(DATA + WORKING)]);
+
+ gServer.registerFile("/" + file.leafName, file);
+ serveUpdateRDF(file.leafName);
+
+ let addon = yield promiseAddonByID(ID);
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, expectedError);
+ do_check_eq(install.addon, null);
+
+ gServer.registerFile("/" + file.leafName, null);
+ gServer.registerPathHandler("/update.rdf", null);
+
+ addon.uninstall();
+}
+
+function* test_update_working(file, expectedSignedState) {
+ // First install the older version
+ yield promiseInstallAllFiles([do_get_file(DATA + WORKING)]);
+
+ gServer.registerFile("/" + file.leafName, file);
+ serveUpdateRDF(file.leafName);
+
+ let addon = yield promiseAddonByID(ID);
+ let update = yield promiseFindAddonUpdates(addon);
+ let install = update.updateAvailable;
+ yield promiseCompleteAllInstalls([install]);
+
+ do_check_eq(install.state, AddonManager.STATE_INSTALLED);
+ do_check_neq(install.addon, null);
+ do_check_eq(install.addon.signedState, expectedSignedState);
+
+ gServer.registerFile("/" + file.leafName, null);
+ gServer.registerPathHandler("/update.rdf", null);
+
+ install.addon.uninstall();
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+ startupManager();
+
+ run_next_test();
+}
+
+// Try to install a broken add-on
+add_task(function*() {
+ let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+add_task(function*() {
+ let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+add_task(function*() {
+ let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+// Try to install an add-on with an incorrect ID
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.badid);
+ yield test_install_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+});
+
+// Try to install an unsigned add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.unsigned);
+ yield test_install_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED);
+});
+
+// Try to install a preliminarily reviewed add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.preliminary);
+ yield test_install_working(file, AddonManager.SIGNEDSTATE_PRELIMINARY);
+});
+
+// Try to install a signed add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.signed);
+ yield test_install_working(file, AddonManager.SIGNEDSTATE_SIGNED);
+});
+
+// Try to update to a broken add-on
+add_task(function*() {
+ let file = createBrokenAddonModify(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+add_task(function*() {
+ let file = createBrokenAddonAdd(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+add_task(function*() {
+ let file = createBrokenAddonRemove(do_get_file(DATA + ADDONS.bootstrap.signed));
+ yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+ file.remove(true);
+});
+
+// Try to update to an add-on with an incorrect ID
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.badid);
+ yield test_update_broken(file, AddonManager.ERROR_CORRUPT_FILE);
+});
+
+// Try to update to an unsigned add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.unsigned);
+ yield test_update_broken(file, AddonManager.ERROR_SIGNEDSTATE_REQUIRED);
+});
+
+// Try to update to a preliminarily reviewed add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.preliminary);
+ yield test_update_working(file, AddonManager.SIGNEDSTATE_PRELIMINARY);
+});
+
+// Try to update to a signed add-on
+add_task(function*() {
+ let file = do_get_file(DATA + ADDONS.bootstrap.signed);
+ yield test_update_working(file, AddonManager.SIGNEDSTATE_SIGNED);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js
new file mode 100644
index 000000000..b74d7804a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_long.js
@@ -0,0 +1,49 @@
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+gUseRealCertChecks = true;
+
+const DATA = "data/signing_checks/";
+
+const ID_63 = "123456789012345678901234567890123456789012345@tests.mozilla.org"
+const ID_64 = "1234567890123456789012345678901234567890123456@tests.mozilla.org"
+const ID_65 = "12345678901234567890123456789012345678901234568@tests.mozilla.org"
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ startupManager();
+
+ run_next_test();
+}
+
+// Installs the cases that should be working
+add_task(function* test_working() {
+ yield promiseInstallAllFiles([do_get_file(DATA + "long_63_plain.xpi"),
+ do_get_file(DATA + "long_64_plain.xpi"),
+ do_get_file(DATA + "long_65_hash.xpi")]);
+
+ let addons = yield promiseAddonsByIDs([ID_63, ID_64, ID_65]);
+
+ for (let addon of addons) {
+ do_check_neq(addon, null);
+ do_check_true(addon.signedState > AddonManager.SIGNEDSTATE_MISSING);
+
+ addon.uninstall();
+ }
+});
+
+// Checks the cases that should be broken
+add_task(function* test_broken() {
+ function promiseInstallForFile(file) {
+ return new Promise(resolve => AddonManager.getInstallForFile(file, resolve));
+ }
+
+ let promises = [promiseInstallForFile(do_get_file(DATA + "long_63_hash.xpi")),
+ promiseInstallForFile(do_get_file(DATA + "long_64_hash.xpi"))];
+ let installs = yield Promise.all(promises);
+
+ for (let install of installs) {
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ do_check_eq(install.error, AddonManager.ERROR_CORRUPT_FILE);
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
new file mode 100644
index 000000000..97e2ff79f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_migrate.js
@@ -0,0 +1,194 @@
+// Enable signature checks for these tests
+gUseRealCertChecks = true;
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+// Allow attempting to show the compatibility UI which should not happen
+Services.prefs.setBoolPref("extensions.showMismatchUI", true);
+
+const DATA = "data/signing_checks/";
+const ADDONS = {
+ bootstrap: {
+ unsigned: "unsigned_bootstrap_2.xpi",
+ badid: "signed_bootstrap_badid_2.xpi",
+ signed: "signed_bootstrap_2.xpi",
+ },
+ nonbootstrap: {
+ unsigned: "unsigned_nonbootstrap_2.xpi",
+ badid: "signed_nonbootstrap_badid_2.xpi",
+ signed: "signed_nonbootstrap_2.xpi",
+ }
+};
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Override the window watcher
+var WindowWatcher = {
+ sawAddon: false,
+
+ openWindow: function(parent, url, name, features, args) {
+ let ids = args.QueryInterface(AM_Ci.nsIVariant);
+ this.sawAddon = ids.indexOf(ID) >= 0;
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(AM_Ci.nsIWindowWatcher)
+ || iid.equals(AM_Ci.nsISupports))
+ return this;
+
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+}
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+function resetPrefs() {
+ Services.prefs.setIntPref("bootstraptest.active_version", -1);
+ Services.prefs.setIntPref("bootstraptest.installed_version", -1);
+ Services.prefs.setIntPref("bootstraptest.startup_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.shutdown_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.install_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.uninstall_reason", -1);
+ Services.prefs.setIntPref("bootstraptest.startup_oldversion", -1);
+ Services.prefs.setIntPref("bootstraptest.shutdown_newversion", -1);
+ Services.prefs.setIntPref("bootstraptest.install_oldversion", -1);
+ Services.prefs.setIntPref("bootstraptest.uninstall_newversion", -1);
+}
+
+function getActiveVersion() {
+ return Services.prefs.getIntPref("bootstraptest.active_version");
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+ // Start and stop the manager to initialise everything in the profile before
+ // actual testing
+ startupManager();
+ shutdownManager();
+ resetPrefs();
+
+ run_next_test();
+}
+
+// Removes the signedState field from add-ons in the json database to make it
+// look like the database was written with an older version of the application
+function stripDB() {
+ let jData = loadJSON(gExtensionsJSON);
+ jData.schemaVersion--;
+
+ for (let addon of jData.addons)
+ delete addon.signedState;
+
+ saveJSON(jData, gExtensionsJSON);
+}
+
+function* test_breaking_migrate(addons, test, expectedSignedState) {
+ // Startup as the old version
+ gAppInfo.version = "4";
+ startupManager(true);
+
+ // Install the signed add-on
+ yield promiseInstallAllFiles([do_get_file(DATA + addons.signed)]);
+ // Restart to let non-restartless add-ons install fully
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+ resetPrefs();
+ stripDB();
+
+ // Now replace it with the version to test. Doing this so quickly shouldn't
+ // trigger the file modification code to detect the change by itself.
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
+
+ // Update the application
+ gAppInfo.version = "5";
+ startupManager(true);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, expectedSignedState);
+
+ // Add-on shouldn't be active
+ if (addons == ADDONS.bootstrap)
+ do_check_eq(getActiveVersion(), -1);
+ else
+ do_check_false(isExtensionInAddonsList(profileDir, ID));
+
+ // Should have flagged the change during startup
+ let changes = AddonManager.getStartupChanges(AddonManager.STARTUP_CHANGE_DISABLED);
+ do_check_eq(changes.length, 1);
+ do_check_eq(changes[0], ID);
+
+ // Shouldn't have checked for updates for the add-on
+ do_check_false(WindowWatcher.sawAddon);
+ WindowWatcher.sawAddon = false;
+
+ addon.uninstall();
+ // Restart to let non-restartless add-ons uninstall fully
+ yield promiseRestartManager();
+ yield shutdownManager();
+ resetPrefs();
+}
+
+function* test_working_migrate(addons, test, expectedSignedState) {
+ // Startup as the old version
+ gAppInfo.version = "4";
+ startupManager(true);
+
+ // Install the signed add-on
+ yield promiseInstallAllFiles([do_get_file(DATA + addons.signed)]);
+ // Restart to let non-restartless add-ons install fully
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+ resetPrefs();
+ stripDB();
+
+ // Now replace it with the version to test. Doing this so quickly shouldn't
+ // trigger the file modification code to detect the change by itself.
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_file(DATA + addons[test]), profileDir, ID);
+
+ // Update the application
+ gAppInfo.version = "5";
+ startupManager(true);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, expectedSignedState);
+
+ if (addons == ADDONS.bootstrap)
+ do_check_eq(getActiveVersion(), 2);
+ else
+ do_check_true(isExtensionInAddonsList(profileDir, ID));
+
+ // Shouldn't have checked for updates for the add-on
+ do_check_false(WindowWatcher.sawAddon);
+ WindowWatcher.sawAddon = false;
+
+ addon.uninstall();
+ // Restart to let non-restartless add-ons uninstall fully
+ yield promiseRestartManager();
+ yield shutdownManager();
+ resetPrefs();
+}
+
+add_task(function*() {
+ yield test_breaking_migrate(ADDONS.bootstrap, "unsigned", AddonManager.SIGNEDSTATE_MISSING);
+ yield test_breaking_migrate(ADDONS.nonbootstrap, "unsigned", AddonManager.SIGNEDSTATE_MISSING);
+});
+
+add_task(function*() {
+ yield test_breaking_migrate(ADDONS.bootstrap, "badid", AddonManager.SIGNEDSTATE_BROKEN);
+ yield test_breaking_migrate(ADDONS.nonbootstrap, "badid", AddonManager.SIGNEDSTATE_BROKEN);
+});
+
+add_task(function*() {
+ yield test_working_migrate(ADDONS.bootstrap, "signed", AddonManager.SIGNEDSTATE_SIGNED);
+ yield test_working_migrate(ADDONS.nonbootstrap, "signed", AddonManager.SIGNEDSTATE_SIGNED);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_multi.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_multi.js
new file mode 100644
index 000000000..01de29088
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_multi.js
@@ -0,0 +1,55 @@
+// Enable signature checks for these tests
+gUseRealCertChecks = true;
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+
+// Each multi-package XPI contains one valid theme and one other add-on that
+// has the following error state:
+const ADDONS = {
+ "multi_signed.xpi": 0,
+ "multi_badid.xpi": AddonManager.ERROR_CORRUPT_FILE,
+ "multi_broken.xpi": AddonManager.ERROR_CORRUPT_FILE,
+ "multi_unsigned.xpi": AddonManager.ERROR_SIGNEDSTATE_REQUIRED,
+};
+
+function createInstall(filename) {
+ return new Promise(resolve => {
+ AddonManager.getInstallForFile(do_get_file(DATA + filename), resolve, "application/x-xpinstall");
+ });
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+ startupManager();
+
+ run_next_test();
+}
+
+function* test_addon(filename) {
+ do_print("Testing " + filename);
+
+ let install = yield createInstall(filename);
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_eq(install.error, 0);
+
+ do_check_neq(install.linkedInstalls, null);
+ do_check_eq(install.linkedInstalls.length, 1);
+
+ let linked = install.linkedInstalls[0];
+ do_print(linked.state);
+ do_check_eq(linked.error, ADDONS[filename]);
+ if (linked.error == 0) {
+ do_check_eq(linked.state, AddonManager.STATE_DOWNLOADED);
+ linked.cancel();
+ }
+ else {
+ do_check_eq(linked.state, AddonManager.STATE_DOWNLOAD_FAILED);
+ }
+
+ install.cancel();
+}
+
+for (let filename of Object.keys(ADDONS))
+ add_task(test_addon.bind(null, filename));
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js
new file mode 100644
index 000000000..eb1bb78b3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_updatepref.js
@@ -0,0 +1,136 @@
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+gUseRealCertChecks = true;
+
+const DATA = "data/signing_checks/";
+const ID = "test@tests.mozilla.org";
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start();
+
+gServer.registerPathHandler("/update.rdf", function(request, response) {
+ let updateData = {};
+ updateData[ID] = [{
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "4",
+ maxVersion: "6"
+ }]
+ }];
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ response.write(createUpdateRDF(updateData));
+});
+
+const SERVER = "127.0.0.1:" + gServer.identity.primaryPort;
+Services.prefs.setCharPref("extensions.update.background.url", "http://" + SERVER + "/update.rdf");
+
+function verifySignatures() {
+ return new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ Services.obs.removeObserver(observer, "xpi-signature-changed");
+ resolve(JSON.parse(data));
+ }
+ Services.obs.addObserver(observer, "xpi-signature-changed", false);
+
+ do_print("Verifying signatures");
+ let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+ XPIscope.XPIProvider.verifySignatures();
+ });
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+ // Start and stop the manager to initialise everything in the profile before
+ // actual testing
+ startupManager();
+ shutdownManager();
+
+ run_next_test();
+}
+
+// Updating the pref without changing the app version won't disable add-ons
+// immediately but will after a signing check
+add_task(function*() {
+ Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false);
+ startupManager();
+
+ // Install the signed add-on
+ yield promiseInstallAllFiles([do_get_file(DATA + "unsigned_bootstrap_2.xpi")]);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ yield promiseShutdownManager();
+
+ Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ // Update checks shouldn't affect the add-on
+ yield AddonManagerInternal.backgroundUpdateCheck();
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ let changes = yield verifySignatures();
+
+ do_check_eq(changes.disabled.length, 1);
+ do_check_eq(changes.disabled[0], ID);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ addon.uninstall();
+
+ yield promiseShutdownManager();
+});
+
+// Updating the pref with changing the app version will disable add-ons
+// immediately
+add_task(function*() {
+ Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, false);
+ startupManager();
+
+ // Install the signed add-on
+ yield promiseInstallAllFiles([do_get_file(DATA + "unsigned_bootstrap_2.xpi")]);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ yield promiseShutdownManager();
+
+ Services.prefs.setBoolPref(PREF_XPI_SIGNATURES_REQUIRED, true);
+ gAppInfo.version = 5.0
+ startupManager(true);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.appDisabled);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_MISSING);
+
+ addon.uninstall();
+
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js
new file mode 100644
index 000000000..0b5b30d89
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_signed_verify.js
@@ -0,0 +1,234 @@
+// Enable signature checks for these tests
+gUseRealCertChecks = true;
+// Disable update security
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+const DATA = "data/signing_checks/";
+const GOOD = [
+ ["signed_bootstrap_2.xpi", AddonManager.SIGNEDSTATE_SIGNED],
+ ["signed_nonbootstrap_2.xpi", AddonManager.SIGNEDSTATE_SIGNED]
+];
+const BAD = [
+ ["unsigned_bootstrap_2.xpi", AddonManager.SIGNEDSTATE_MISSING],
+ ["signed_bootstrap_badid_2.xpi", AddonManager.SIGNEDSTATE_BROKEN],
+ ["unsigned_nonbootstrap_2.xpi", AddonManager.SIGNEDSTATE_MISSING],
+ ["signed_nonbootstrap_badid_2.xpi", AddonManager.SIGNEDSTATE_BROKEN],
+];
+const ID = "test@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function verifySignatures() {
+ return new Promise(resolve => {
+ let observer = (subject, topic, data) => {
+ Services.obs.removeObserver(observer, "xpi-signature-changed");
+ resolve(JSON.parse(data));
+ }
+ Services.obs.addObserver(observer, "xpi-signature-changed", false);
+
+ do_print("Verifying signatures");
+ let XPIscope = Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm");
+ XPIscope.XPIProvider.verifySignatures();
+ });
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "4", "4");
+
+ run_next_test();
+}
+
+function verify_no_change([startFile, startState], [endFile, endState]) {
+ add_task(function*() {
+ do_print("A switch from " + startFile + " to " + endFile + " should cause no change.");
+
+ // Install the first add-on
+ manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ let wasAppDisabled = addon.appDisabled;
+ do_check_neq(addon.appDisabled, addon.isActive);
+ do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_eq(addon.signedState, startState);
+
+ // Swap in the files from the next add-on
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+ let events = {
+ [ID]: []
+ };
+
+ if (startState != endState)
+ events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+ prepare_test(events);
+
+ // Trigger the check
+ let changes = yield verifySignatures();
+ do_check_eq(changes.enabled.length, 0);
+ do_check_eq(changes.disabled.length, 0);
+
+ do_check_eq(addon.appDisabled, wasAppDisabled);
+ do_check_neq(addon.appDisabled, addon.isActive);
+ do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_eq(addon.signedState, endState);
+
+ // Remove the add-on and restart to let it go away
+ manuallyUninstall(profileDir, ID);
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+ });
+}
+
+function verify_enables([startFile, startState], [endFile, endState]) {
+ add_task(function*() {
+ do_print("A switch from " + startFile + " to " + endFile + " should enable the add-on.");
+
+ // Install the first add-on
+ manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_false(addon.isActive);
+ do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_eq(addon.signedState, startState);
+
+ // Swap in the files from the next add-on
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+ let needsRestart = hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+ do_print(needsRestart);
+
+ let events = {};
+ if (!needsRestart) {
+ events[ID] = [
+ ["onPropertyChanged", ["appDisabled"]],
+ ["onEnabling", false],
+ "onEnabled"
+ ];
+ }
+ else {
+ events[ID] = [
+ ["onPropertyChanged", ["appDisabled"]],
+ "onEnabling"
+ ];
+ }
+
+ if (startState != endState)
+ events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+ prepare_test(events);
+
+ // Trigger the check
+ let changes = yield verifySignatures();
+ do_check_eq(changes.enabled.length, 1);
+ do_check_eq(changes.enabled[0], ID);
+ do_check_eq(changes.disabled.length, 0);
+
+ do_check_false(addon.appDisabled);
+ if (needsRestart)
+ do_check_neq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ else
+ do_check_true(addon.isActive);
+ do_check_eq(addon.signedState, endState);
+
+ ensure_test_completed();
+
+ // Remove the add-on and restart to let it go away
+ manuallyUninstall(profileDir, ID);
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+ });
+}
+
+function verify_disables([startFile, startState], [endFile, endState]) {
+ add_task(function*() {
+ do_print("A switch from " + startFile + " to " + endFile + " should disable the add-on.");
+
+ // Install the first add-on
+ manuallyInstall(do_get_file(DATA + startFile), profileDir, ID);
+ startupManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_eq(addon.signedState, startState);
+
+ let needsRestart = hasFlag(addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_DISABLE);
+
+ // Swap in the files from the next add-on
+ manuallyUninstall(profileDir, ID);
+ manuallyInstall(do_get_file(DATA + endFile), profileDir, ID);
+
+ let events = {};
+ if (!needsRestart) {
+ events[ID] = [
+ ["onPropertyChanged", ["appDisabled"]],
+ ["onDisabling", false],
+ "onDisabled"
+ ];
+ }
+ else {
+ events[ID] = [
+ ["onPropertyChanged", ["appDisabled"]],
+ "onDisabling"
+ ];
+ }
+
+ if (startState != endState)
+ events[ID].unshift(["onPropertyChanged", ["signedState"]]);
+
+ prepare_test(events);
+
+ // Trigger the check
+ let changes = yield verifySignatures();
+ do_check_eq(changes.enabled.length, 0);
+ do_check_eq(changes.disabled.length, 1);
+ do_check_eq(changes.disabled[0], ID);
+
+ do_check_true(addon.appDisabled);
+ if (needsRestart)
+ do_check_neq(addon.pendingOperations, AddonManager.PENDING_NONE);
+ else
+ do_check_false(addon.isActive);
+ do_check_eq(addon.signedState, endState);
+
+ ensure_test_completed();
+
+ // Remove the add-on and restart to let it go away
+ manuallyUninstall(profileDir, ID);
+ yield promiseRestartManager();
+ yield promiseShutdownManager();
+ });
+}
+
+for (let start of GOOD) {
+ for (let end of BAD) {
+ verify_disables(start, end);
+ }
+}
+
+for (let start of BAD) {
+ for (let end of GOOD) {
+ verify_enables(start, end);
+ }
+}
+
+for (let start of GOOD) {
+ for (let end of GOOD.filter(f => f != start)) {
+ verify_no_change(start, end);
+ }
+}
+
+for (let start of BAD) {
+ for (let end of BAD.filter(f => f != start)) {
+ verify_no_change(start, end);
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_softblocked.js b/toolkit/mozapps/extensions/test/xpcshell/test_softblocked.js
new file mode 100644
index 000000000..260b536e1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_softblocked.js
@@ -0,0 +1,109 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const { utils: Cu, interfaces: Ci, classes: Cc, results: Cr } = Components;
+
+const URI_EXTENSION_BLOCKLIST_DIALOG = "chrome://mozapps/content/extensions/blocklist.xul";
+
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+// Allow insecure updates
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false)
+
+const testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+testserver.registerDirectory("/data/", do_get_file("data"));
+
+// Don't need the full interface, attempts to call other methods will just
+// throw which is just fine
+var WindowWatcher = {
+ openWindow: function(parent, url, name, features, openArgs) {
+ // Should be called to list the newly blocklisted items
+ do_check_eq(url, URI_EXTENSION_BLOCKLIST_DIALOG);
+
+ // Simulate auto-disabling any softblocks
+ var list = openArgs.wrappedJSObject.list;
+ list.forEach(function(aItem) {
+ if (!aItem.blocked)
+ aItem.disable = true;
+ });
+
+ // run the code after the blocklist is closed
+ Services.obs.notifyObservers(null, "addon-blocklist-closed", null);
+ },
+
+ QueryInterface: function(iid) {
+ if (iid.equals(Ci.nsIWindowWatcher)
+ || iid.equals(Ci.nsISupports))
+ return this;
+
+ throw Cr.NS_ERROR_NO_INTERFACE;
+ }
+};
+
+MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1", WindowWatcher);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function load_blocklist(aFile) {
+ return new Promise((resolve, reject) => {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "blocklist-updated");
+
+ resolve();
+ }, "blocklist-updated", false);
+
+ Services.prefs.setCharPref("extensions.blocklist.url", `http://localhost:${gPort}/data/${aFile}`);
+ var blocklist = Cc["@mozilla.org/extensions/blocklist;1"].
+ getService(Ci.nsITimerCallback);
+ blocklist.notify(null);
+ });
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ run_next_test();
+}
+
+// Tests that an appDisabled add-on that becomes softBlocked remains disabled
+// when becoming appEnabled
+add_task(function* () {
+ writeInstallRDFForExtension({
+ id: "softblock1@tests.mozilla.org",
+ version: "1.0",
+ name: "Softblocked add-on",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "3"
+ }]
+ }, profileDir);
+
+ startupManager();
+
+ let s1 = yield promiseAddonByID("softblock1@tests.mozilla.org");
+
+ // Make sure to mark it as previously enabled.
+ s1.userDisabled = false;
+
+ do_check_false(s1.softDisabled);
+ do_check_true(s1.appDisabled);
+ do_check_false(s1.isActive);
+
+ yield load_blocklist("test_softblocked1.xml");
+
+ do_check_true(s1.softDisabled);
+ do_check_true(s1.appDisabled);
+ do_check_false(s1.isActive);
+
+ yield promiseRestartManager("2");
+
+ s1 = yield promiseAddonByID("softblock1@tests.mozilla.org");
+
+ do_check_true(s1.softDisabled);
+ do_check_false(s1.appDisabled);
+ do_check_false(s1.isActive);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js b/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js
new file mode 100644
index 000000000..db5e4f7cc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_sourceURI.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/httpd.js");
+var gServer = new HttpServer();
+gServer.start(-1);
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+
+const PORT = gServer.identity.primaryPort;
+const BASE_URL = "http://localhost:" + PORT;
+const DEFAULT_URL = "about:blank";
+
+var addon = {
+ id: "addon@tests.mozilla.org",
+ version: "1.0",
+ name: "Test",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function backgroundUpdate(aCallback) {
+ Services.obs.addObserver(function() {
+ Services.obs.removeObserver(arguments.callee, "addons-background-update-complete");
+ aCallback();
+ }, "addons-background-update-complete", false);
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+}
+
+function run_test() {
+ do_test_pending();
+
+ mapUrlToFile("/cache.xml", do_get_file("data/test_sourceURI.xml"), gServer);
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS, BASE_URL + "/cache.xml");
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE, BASE_URL + "/cache.xml");
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+ writeInstallRDFForExtension(addon, profileDir);
+ startupManager();
+
+ AddonManager.getAddonByID("addon@tests.mozilla.org", function(a) {
+ do_check_neq(a, null);
+ do_check_eq(a.sourceURI, null);
+
+ backgroundUpdate(function() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_neq(a2.sourceURI, null);
+ do_check_eq(a2.sourceURI.spec, "http://www.example.com/testaddon.xpi");
+
+ do_test_finished();
+ });
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_startup.js b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
new file mode 100644
index 000000000..fdd00c1ad
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_startup.js
@@ -0,0 +1,932 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies startup detection of added/removed/changed items and install
+// location priorities
+
+// Enable loading extensions from the user and system scopes
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE + AddonManager.SCOPE_USER +
+ AddonManager.SCOPE_SYSTEM);
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }, { // Repeated target application entries should be ignored
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }]
+};
+
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "2.0",
+ name: "Test 2",
+ targetApplications: [{ // Bad target application entries should be ignored
+ minVersion: "3",
+ maxVersion: "4"
+ }, {
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+};
+
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "3.0",
+ name: "Test 3",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9.2",
+ maxVersion: "1.9.2.*"
+ }]
+};
+
+// Should be ignored because it has no ID
+var addon4 = {
+ version: "4.0",
+ name: "Test 4",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Should be ignored because it has no version
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Should be ignored because it has an invalid type
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "3.0",
+ name: "Test 6",
+ type: 5,
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9.2",
+ maxVersion: "1.9.2.*"
+ }]
+};
+
+// Should be ignored because it has an invalid type
+var addon7 = {
+ id: "addon7@tests.mozilla.org",
+ version: "3.0",
+ name: "Test 3",
+ type: "extension",
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "1.9.2",
+ maxVersion: "1.9.2.*"
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const globalDir = gProfD.clone();
+globalDir.append("extensions2");
+globalDir.append(gAppInfo.ID);
+registerDirectory("XRESysSExtPD", globalDir.parent);
+const userDir = gProfD.clone();
+userDir.append("extensions3");
+userDir.append(gAppInfo.ID);
+registerDirectory("XREUSysExt", userDir.parent);
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var gCachePurged = false;
+
+// Set up the profile
+function run_test() {
+ do_test_pending("test_startup main");
+
+ let obs = AM_Cc["@mozilla.org/observer-service;1"].
+ getService(AM_Ci.nsIObserverService);
+ obs.addObserver({
+ observe: function(aSubject, aTopic, aData) {
+ gCachePurged = true;
+ }
+ }, "startupcache-invalidate", false);
+
+ startupManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+
+ do_check_false(gExtensionsJSON.exists());
+
+ do_check_false(gExtensionsINI.exists());
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7]) {
+
+ do_check_eq(a1, null);
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+ do_check_eq(a2, null);
+ do_check_not_in_crash_annotation(addon2.id, addon2.version);
+ do_check_eq(a3, null);
+ do_check_not_in_crash_annotation(addon3.id, addon3.version);
+ do_check_eq(a4, null);
+ do_check_eq(a5, null);
+
+ do_execute_soon(run_test_1);
+ });
+}
+
+function end_test() {
+ do_test_finished("test_startup main");
+}
+
+// Try to install all the items into the profile
+function run_test_1() {
+ writeInstallRDFForExtension(addon1, profileDir);
+ var dest = writeInstallRDFForExtension(addon2, profileDir);
+ // Attempt to make this look like it was added some time in the past so
+ // the change in run_test_2 makes the last modified time change.
+ setExtensionModifiedTime(dest, dest.lastModifiedTime - 5000);
+
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir, "addon4@tests.mozilla.org");
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ do_print("Checking for " + gExtensionsINI.path);
+ do_check_true(gExtensionsINI.exists());
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_neq(a1.syncGUID, null);
+ do_check_true(a1.syncGUID.length >= 9);
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.name, "Test 1");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_eq(a1.sourceURI, null);
+ do_check_true(a1.foreignInstall);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.seen);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_neq(a2.syncGUID, null);
+ do_check_true(a2.syncGUID.length >= 9);
+ do_check_eq(a2.version, "2.0");
+ do_check_eq(a2.name, "Test 2");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, addon2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_PROFILE);
+ do_check_eq(a2.sourceURI, null);
+ do_check_true(a2.foreignInstall);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.seen);
+
+ do_check_neq(a3, null);
+ do_check_eq(a3.id, "addon3@tests.mozilla.org");
+ do_check_neq(a3.syncGUID, null);
+ do_check_true(a3.syncGUID.length >= 9);
+ do_check_eq(a3.version, "3.0");
+ do_check_eq(a3.name, "Test 3");
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_true(hasFlag(a3.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a3.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon3.id, addon3.version);
+ do_check_eq(a3.scope, AddonManager.SCOPE_PROFILE);
+ do_check_eq(a3.sourceURI, null);
+ do_check_true(a3.foreignInstall);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.seen);
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
+ do_check_false(dest.exists());
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon5@tests.mozilla.org"));
+ do_check_false(dest.exists());
+
+ do_check_eq(a6, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon6@tests.mozilla.org"));
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon6@tests.mozilla.org"));
+ do_check_false(dest.exists());
+
+ do_check_eq(a7, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon7@tests.mozilla.org"));
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon7@tests.mozilla.org"));
+ do_check_false(dest.exists());
+
+ AddonManager.getAddonsByTypes(["extension"], function(extensionAddons) {
+ do_check_eq(extensionAddons.length, 3);
+
+ do_execute_soon(run_test_2);
+ });
+ });
+}
+
+// Test that modified items are detected and items in other install locations
+// are ignored
+function run_test_2() {
+ addon1.version = "1.1";
+ writeInstallRDFForExtension(addon1, userDir);
+ addon2.version="2.1";
+ writeInstallRDFForExtension(addon2, profileDir);
+ addon2.version="2.2";
+ writeInstallRDFForExtension(addon2, globalDir);
+ addon2.version="2.3";
+ writeInstallRDFForExtension(addon2, userDir);
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon3@tests.mozilla.org"));
+ dest.remove(true);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon3@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ do_check_true(gExtensionsINI.exists());
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.0");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_false(isExtensionInAddonsList(userDir, a1.id));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_true(a1.foreignInstall);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.1");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_PROFILE);
+ do_check_true(a2.foreignInstall);
+
+ do_check_eq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+ do_check_not_in_crash_annotation(addon3.id, addon3.version);
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Check that removing items from the profile reveals their hidden versions.
+function run_test_3() {
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest.remove(true);
+ writeInstallRDFForExtension(addon3, profileDir, "addon4@tests.mozilla.org");
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.1");
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(isExtensionInAddonsList(userDir, a1.id));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_USER);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.3");
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_check_eq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
+ do_check_false(dest.exists());
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Test that disabling an install location works
+function run_test_4() {
+ Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_SYSTEM);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon1@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_eq(a1, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon1@tests.mozilla.org"));
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.2");
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_true(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_execute_soon(run_test_5);
+ });
+}
+
+// Switching disabled locations works
+function run_test_5() {
+ Services.prefs.setIntPref("extensions.enabledScopes", AddonManager.SCOPE_USER);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon1@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.1");
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(isExtensionInAddonsList(userDir, a1.id));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_USER);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.3");
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+// Resetting the pref makes everything visible again
+function run_test_6() {
+ Services.prefs.clearUserPref("extensions.enabledScopes");
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.1");
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(isExtensionInAddonsList(userDir, a1.id));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_USER);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.3");
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_true(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_USER);
+
+ do_execute_soon(run_test_7);
+ });
+}
+
+// Check that items in the profile hide the others again.
+function run_test_7() {
+ addon1.version = "1.2";
+ writeInstallRDFForExtension(addon1, profileDir);
+ var dest = userDir.clone();
+ dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest.remove(true);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.2");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_false(isExtensionInAddonsList(userDir, a1.id));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.2");
+ do_check_false(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_true(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon2.id, a2.version);
+ do_check_eq(a2.scope, AddonManager.SCOPE_SYSTEM);
+
+ do_check_eq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Disabling all locations still leaves the profile working
+function run_test_8() {
+ Services.prefs.setIntPref("extensions.enabledScopes", 0);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.2");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_false(isExtensionInAddonsList(userDir, a1.id));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_in_crash_annotation(addon1.id, a1.version);
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ do_check_eq(a2, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon2@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon2@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon2@tests.mozilla.org"));
+
+ do_execute_soon(run_test_9);
+ });
+}
+
+// More hiding and revealing
+function run_test_9() {
+ Services.prefs.clearUserPref("extensions.enabledScopes", 0);
+
+ var dest = userDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+ dest = globalDir.clone();
+ dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest.remove(true);
+ addon2.version = "2.4";
+ writeInstallRDFForExtension(addon2, profileDir);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, ["addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.2");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_false(isExtensionInAddonsList(userDir, a1.id));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_eq(a1.scope, AddonManager.SCOPE_PROFILE);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.4");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_eq(a2.scope, AddonManager.SCOPE_PROFILE);
+
+ do_check_eq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+
+ do_execute_soon(run_test_10);
+ });
+}
+
+// Checks that a removal from one location and an addition in another location
+// for the same item is handled
+function run_test_10() {
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+ addon1.version = "1.3";
+ writeInstallRDFForExtension(addon1, userDir);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, ["addon1@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_eq(a1.id, "addon1@tests.mozilla.org");
+ do_check_eq(a1.version, "1.3");
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_true(isExtensionInAddonsList(userDir, a1.id));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_false(hasFlag(a1.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_eq(a1.scope, AddonManager.SCOPE_USER);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.id, "addon2@tests.mozilla.org");
+ do_check_eq(a2.version, "2.4");
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_false(isExtensionInAddonsList(userDir, a2.id));
+ do_check_false(isExtensionInAddonsList(globalDir, a2.id));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ do_check_true(hasFlag(a2.permissions, AddonManager.PERM_CAN_UPGRADE));
+ do_check_eq(a2.scope, AddonManager.SCOPE_PROFILE);
+
+ do_check_eq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+
+ do_check_eq(a4, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+
+ do_execute_soon(run_test_11);
+ });
+}
+
+// This should remove any remaining items
+function run_test_11() {
+ var dest = userDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+ dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest.remove(true);
+
+ gCachePurged = false;
+ restartManager();
+ check_startup_changes(AddonManager.STARTUP_CHANGE_INSTALLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_CHANGED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_UNINSTALLED, ["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org"]);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_DISABLED, []);
+ check_startup_changes(AddonManager.STARTUP_CHANGE_ENABLED, []);
+ do_check_true(gCachePurged);
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_eq(a1, null);
+ do_check_eq(a2, null);
+ do_check_eq(a3, null);
+ do_check_eq(a4, null);
+ do_check_eq(a5, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(profileDir, "addon2@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(profileDir, "addon3@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(profileDir, "addon4@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(profileDir, "addon5@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon1@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon2@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon3@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon4@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(userDir, "addon5@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon1@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon2@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon3@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon4@tests.mozilla.org"));
+ do_check_false(isExtensionInAddonsList(globalDir, "addon5@tests.mozilla.org"));
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+ do_check_not_in_crash_annotation(addon2.id, addon2.version);
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+// Test that auto-disabling for specific scopes works
+function run_test_12() {
+ Services.prefs.setIntPref("extensions.autoDisableScopes", AddonManager.SCOPE_USER);
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, userDir);
+ writeInstallRDFForExtension(addon3, globalDir);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ callback_soon(function([a1, a2, a3, a4, a5]) {
+ do_check_neq(a1, null);
+ do_check_false(a1.userDisabled);
+ do_check_true(a1.seen);
+ do_check_true(a1.isActive);
+
+ do_check_neq(a2, null);
+ do_check_true(a2.userDisabled);
+ do_check_false(a2.seen);
+ do_check_false(a2.isActive);
+
+ do_check_neq(a3, null);
+ do_check_false(a3.userDisabled);
+ do_check_true(a3.seen);
+ do_check_true(a3.isActive);
+
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest.remove(true);
+ dest = userDir.clone();
+ dest.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest.remove(true);
+ dest = globalDir.clone();
+ dest.append(do_get_expected_addon_name("addon3@tests.mozilla.org"));
+ dest.remove(true);
+
+ restartManager();
+
+ Services.prefs.setIntPref("extensions.autoDisableScopes", AddonManager.SCOPE_SYSTEM);
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, userDir);
+ writeInstallRDFForExtension(addon3, globalDir);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1_2, a2_2, a3_2, a4_2, a5_2]) {
+ do_check_neq(a1_2, null);
+ do_check_false(a1_2.userDisabled);
+ do_check_true(a1_2.seen);
+ do_check_true(a1_2.isActive);
+
+ do_check_neq(a2_2, null);
+ do_check_false(a2_2.userDisabled);
+ do_check_true(a2_2.seen);
+ do_check_true(a2_2.isActive);
+
+ do_check_neq(a3_2, null);
+ do_check_true(a3_2.userDisabled);
+ do_check_false(a3_2.seen);
+ do_check_false(a3_2.isActive);
+
+ var dest2 = profileDir.clone();
+ dest2.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ dest2.remove(true);
+ dest2 = userDir.clone();
+ dest2.append(do_get_expected_addon_name("addon2@tests.mozilla.org"));
+ dest2.remove(true);
+ dest2 = globalDir.clone();
+ dest2.append(do_get_expected_addon_name("addon3@tests.mozilla.org"));
+ dest2.remove(true);
+
+ restartManager();
+
+ Services.prefs.setIntPref("extensions.autoDisableScopes", AddonManager.SCOPE_USER + AddonManager.SCOPE_SYSTEM);
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, userDir);
+ writeInstallRDFForExtension(addon3, globalDir);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1_3, a2_3, a3_3, a4_3, a5_3]) {
+ do_check_neq(a1_3, null);
+ do_check_false(a1_3.userDisabled);
+ do_check_true(a1_3.seen);
+ do_check_true(a1_3.isActive);
+
+ do_check_neq(a2_3, null);
+ do_check_true(a2_3.userDisabled);
+ do_check_false(a2_3.seen);
+ do_check_false(a2_3.isActive);
+
+ do_check_neq(a3_3, null);
+ do_check_true(a3_3.userDisabled);
+ do_check_false(a3_3.seen);
+ do_check_false(a3_3.isActive);
+
+ do_execute_soon(end_test);
+ });
+ });
+ }));
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js
new file mode 100644
index 000000000..cb6704936
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_strictcompatibility.js
@@ -0,0 +1,203 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Tests AddonManager.strictCompatibility and it's related preference,
+// extensions.strictCompatibility, and the strictCompatibility option in
+// install.rdf
+
+
+// Always compatible
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Incompatible in strict compatibility mode
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.7",
+ maxVersion: "0.8"
+ }]
+};
+
+// Theme - always uses strict compatibility, so is always incompatible
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ internalName: "test-theme-3",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.8",
+ maxVersion: "0.9"
+ }]
+};
+
+// Opt-in to strict compatibility - always incompatible
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ strictCompatibility: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.8",
+ maxVersion: "0.9"
+ }]
+};
+
+// Addon from the future - would be marked as compatibile-by-default,
+// but minVersion is higher than the app version
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "3",
+ maxVersion: "5"
+ }]
+};
+
+// Extremely old addon - maxVersion is less than the mimimum compat version
+// set in extensions.minCompatibleVersion
+var addon6 = {
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 6",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }]
+};
+
+// Dictionary - incompatible in strict compatibility mode
+var addon7= {
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 7",
+ type: "64",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.8",
+ maxVersion: "0.9"
+ }]
+};
+
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function do_check_compat_status(aStrict, aAddonCompat, aCallback) {
+ do_check_eq(AddonManager.strictCompatibility, aStrict);
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org",
+ "addon7@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5, a6, a7]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.isCompatible, aAddonCompat[0]);
+ do_check_eq(a1.appDisabled, !aAddonCompat[0]);
+ do_check_false(a1.strictCompatibility);
+
+ do_check_neq(a2, null);
+ do_check_eq(a2.isCompatible, aAddonCompat[1]);
+ do_check_eq(a2.appDisabled, !aAddonCompat[1]);
+ do_check_false(a2.strictCompatibility);
+
+ do_check_neq(a3, null);
+ do_check_eq(a3.isCompatible, aAddonCompat[2]);
+ do_check_eq(a3.appDisabled, !aAddonCompat[2]);
+ do_check_true(a3.strictCompatibility);
+
+ do_check_neq(a4, null);
+ do_check_eq(a4.isCompatible, aAddonCompat[3]);
+ do_check_eq(a4.appDisabled, !aAddonCompat[3]);
+ do_check_true(a4.strictCompatibility);
+
+ do_check_neq(a5, null);
+ do_check_eq(a5.isCompatible, aAddonCompat[4]);
+ do_check_eq(a5.appDisabled, !aAddonCompat[4]);
+ do_check_false(a5.strictCompatibility);
+
+ do_check_neq(a6, null);
+ do_check_eq(a6.isCompatible, aAddonCompat[5]);
+ do_check_eq(a6.appDisabled, !aAddonCompat[5]);
+ do_check_false(a6.strictCompatibility);
+
+ do_check_neq(a7, null);
+ do_check_eq(a7.isCompatible, aAddonCompat[6]);
+ do_check_eq(a7.appDisabled, !aAddonCompat[6]);
+ do_check_false(a7.strictCompatibility);
+
+ do_execute_soon(aCallback);
+ });
+}
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+ writeInstallRDFForExtension(addon6, profileDir);
+ writeInstallRDFForExtension(addon7, profileDir);
+
+ Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0.1");
+
+ startupManager();
+
+ // Should default to enabling strict compat.
+ do_check_compat_status(true, [true, false, false, false, false, false, false], run_test_1);
+}
+
+function run_test_1() {
+ do_print("Test 1");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+ do_check_compat_status(false, [true, true, false, false, false, true, true], run_test_2);
+}
+
+function run_test_2() {
+ do_print("Test 2");
+ restartManager();
+ do_check_compat_status(false, [true, true, false, false, false, true, true], run_test_3);
+}
+
+function run_test_3() {
+ do_print("Test 3");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+ do_check_compat_status(true, [true, false, false, false, false, false, false], run_test_4);
+}
+
+function run_test_4() {
+ do_print("Test 4");
+ restartManager();
+ do_check_compat_status(true, [true, false, false, false, false, false, false], run_test_5);
+}
+
+function run_test_5() {
+ do_print("Test 5");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+ Services.prefs.setCharPref(PREF_EM_MIN_COMPAT_APP_VERSION, "0.4");
+ do_check_compat_status(false, [true, true, false, false, false, false, true], do_test_finished);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_switch_os.js b/toolkit/mozapps/extensions/test/xpcshell/test_switch_os.js
new file mode 100644
index 000000000..552d7cfae
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_switch_os.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+const ID = "bootstrap1@tests.mozilla.org";
+
+BootstrapMonitor.init();
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+add_task(function*() {
+ startupManager();
+
+ let install = yield new Promise(resolve => AddonManager.getInstallForFile(do_get_addon("test_bootstrap1_1"), resolve));
+ yield promiseCompleteAllInstalls([install]);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_false(addon.userDisabled);
+ do_check_true(addon.isActive);
+
+ yield promiseShutdownManager();
+
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ let jData = loadJSON(gExtensionsJSON);
+
+ for (let addonInstance of jData.addons) {
+ if (addonInstance.id == ID) {
+ // Set to something that would be an invalid descriptor for this platform
+ addonInstance.descriptor = AppConstants.platform == "win" ? "/foo/bar" : "C:\\foo\\bar";
+ }
+ }
+
+ saveJSON(jData, gExtensionsJSON);
+
+ startupManager();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ BootstrapMonitor.checkAddonStarted(ID);
+ do_check_false(addon.userDisabled);
+ do_check_true(addon.isActive);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js b/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js
new file mode 100644
index 000000000..385f58405
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_syncGUID.js
@@ -0,0 +1,156 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/Services.jsm");
+
+// restartManager() mucks with XPIProvider.jsm importing, so we hack around.
+this.__defineGetter__("XPIProvider", function () {
+ let scope = {};
+ return Components.utils.import("resource://gre/modules/addons/XPIProvider.jsm", scope)
+ .XPIProvider;
+});
+
+const addonId = "addon1@tests.mozilla.org";
+
+function run_test() {
+ Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9");
+ startupManager();
+
+ run_next_test();
+}
+
+const UUID_PATTERN = /^\{[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}$/i;
+
+add_test(function test_getter_and_setter() {
+ // Our test add-on requires a restart.
+ let listener = {
+ onInstallEnded: function onInstallEnded() {
+ AddonManager.removeInstallListener(listener);
+ // never restart directly inside an onInstallEnded handler!
+ do_execute_soon(function getter_setter_install_ended() {
+ restartManager();
+
+ AddonManager.getAddonByID(addonId, function(addon) {
+
+ do_check_neq(addon, null);
+ do_check_neq(addon.syncGUID, null);
+ do_check_true(UUID_PATTERN.test(addon.syncGUID));
+
+ let oldGUID = addon.SyncGUID;
+ let newGUID = "foo";
+
+ addon.syncGUID = newGUID;
+ do_check_eq(newGUID, addon.syncGUID);
+
+ // Verify change made it to DB.
+ AddonManager.getAddonByID(addonId, function(newAddon) {
+ do_check_neq(newAddon, null);
+ do_check_eq(newGUID, newAddon.syncGUID);
+ });
+
+ run_next_test();
+ });
+ });
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+
+ AddonManager.getInstallForFile(do_get_addon("test_install1"),
+ function(install) {
+ install.install();
+ });
+});
+
+add_test(function test_fetch_by_guid_unknown_guid() {
+ XPIProvider.getAddonBySyncGUID("XXXX", function(addon) {
+ do_check_eq(null, addon);
+ run_next_test();
+ });
+});
+
+// Ensure setting an extension to an existing syncGUID results in error.
+add_test(function test_error_on_duplicate_syncguid_insert() {
+ const installNames = ["test_install1", "test_install2_1"];
+ const installIDs = ["addon1@tests.mozilla.org", "addon2@tests.mozilla.org"];
+
+ let installCount = 0;
+
+ let listener = {
+ onInstallEnded: function onInstallEnded() {
+ installCount++;
+
+ if (installCount == installNames.length) {
+ AddonManager.removeInstallListener(listener);
+ do_execute_soon(function duplicate_syncguid_install_ended() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(installIDs, callback_soon(function(addons) {
+ let initialGUID = addons[1].syncGUID;
+
+ try {
+ addons[1].syncGUID = addons[0].syncGUID;
+ do_throw("Should not get here.");
+ }
+ catch (e) {
+ do_check_true(e.message.startsWith("Addon sync GUID conflict"));
+ restartManager();
+
+ AddonManager.getAddonByID(installIDs[1], function(addon) {
+ do_check_eq(initialGUID, addon.syncGUID);
+ run_next_test();
+ });
+ }
+ }));
+ });
+ }
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+ let getInstallCB = function(install) { install.install(); };
+
+ for (let name of installNames) {
+ AddonManager.getInstallForFile(do_get_addon(name), getInstallCB);
+ }
+});
+
+add_test(function test_fetch_by_guid_known_guid() {
+ AddonManager.getAddonByID(addonId, function(addon) {
+ do_check_neq(null, addon);
+ do_check_neq(null, addon.syncGUID);
+
+ let syncGUID = addon.syncGUID;
+
+ XPIProvider.getAddonBySyncGUID(syncGUID, function(newAddon) {
+ do_check_neq(null, newAddon);
+ do_check_eq(syncGUID, newAddon.syncGUID);
+
+ run_next_test();
+ });
+ });
+});
+
+add_test(function test_addon_manager_get_by_sync_guid() {
+ AddonManager.getAddonByID(addonId, function(addon) {
+ do_check_neq(null, addon.syncGUID);
+
+ let syncGUID = addon.syncGUID;
+
+ AddonManager.getAddonBySyncGUID(syncGUID, function(newAddon) {
+ do_check_neq(null, newAddon);
+ do_check_eq(addon.id, newAddon.id);
+ do_check_eq(syncGUID, newAddon.syncGUID);
+
+ AddonManager.getAddonBySyncGUID("DOES_NOT_EXIST", function(missing) {
+ do_check_eq(undefined, missing);
+
+ run_next_test();
+ });
+ });
+ });
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
new file mode 100644
index 000000000..35964c663
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_delay_update.js
@@ -0,0 +1,461 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that delaying a system add-on update works.
+
+Components.utils.import("resource://testing-common/httpd.js");
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+const tempdir = gTmpD.clone();
+
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
+const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url";
+
+const IGNORE_ID = "system_delay_ignore@tests.mozilla.org";
+const COMPLETE_ID = "system_delay_complete@tests.mozilla.org";
+const DEFER_ID = "system_delay_defer@tests.mozilla.org";
+const DEFER_ALSO_ID = "system_delay_defer_also@tests.mozilla.org";
+const NORMAL_ID = "system1@tests.mozilla.org";
+
+
+const TEST_IGNORE_PREF = "delaytest.ignore";
+
+const distroDir = FileUtils.getDir("ProfD", ["sysfeatures"], true);
+registerDirectory("XREAppFeat", distroDir);
+
+let testserver = new HttpServer();
+testserver.registerDirectory("/data/", do_get_file("data/system_addons"));
+testserver.start();
+let root = `${testserver.identity.primaryScheme}://${testserver.identity.primaryHost}:${testserver.identity.primaryPort}/data/`;
+Services.prefs.setCharPref(PREF_SYSTEM_ADDON_UPDATE_URL, root + "update.xml");
+
+
+// Note that we would normally use BootstrapMonitor but it currently requires
+// the objects in `data` to be serializable, and we need a real reference to the
+// `instanceID` symbol to test.
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+
+function promiseInstallPostponed(addonID1, addonID2) {
+ return new Promise((resolve, reject) => {
+ let seen = [];
+ let listener = {
+ onInstallFailed: () => {
+ AddonManager.removeInstallListener(listener);
+ reject("extension installation should not have failed");
+ },
+ onInstallEnded: (install) => {
+ AddonManager.removeInstallListener(listener);
+ reject(`extension installation should not have ended for ${install.addon.id}`);
+ },
+ onInstallPostponed: (install) => {
+ seen.push(install.addon.id);
+ if (seen.includes(addonID1) && seen.includes(addonID2)) {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ }
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+function promiseInstallResumed(addonID1, addonID2) {
+ return new Promise((resolve, reject) => {
+ let seenPostponed = [];
+ let seenEnded = [];
+ let listener = {
+ onInstallFailed: () => {
+ AddonManager.removeInstallListener(listener);
+ reject("extension installation should not have failed");
+ },
+ onInstallEnded: (install) => {
+ seenEnded.push(install.addon.id);
+ if ((seenEnded.includes(addonID1) && seenEnded.includes(addonID2)) &&
+ (seenPostponed.includes(addonID1) && seenPostponed.includes(addonID2))) {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ }
+ },
+ onInstallPostponed: (install) => {
+ seenPostponed.push(install.addon.id);
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+function promiseInstallDeferred(addonID1, addonID2) {
+ return new Promise((resolve, reject) => {
+ let seenEnded = [];
+ let listener = {
+ onInstallFailed: () => {
+ AddonManager.removeInstallListener(listener);
+ reject("extension installation should not have failed");
+ },
+ onInstallEnded: (install) => {
+ seenEnded.push(install.addon.id);
+ if (seenEnded.includes(addonID1) && seenEnded.includes(addonID2)) {
+ AddonManager.removeInstallListener(listener);
+ resolve();
+ }
+ },
+ onInstallPostponed: (install) => {
+ AddonManager.removeInstallListener(listener);
+ reject(`extension installation should not have been postponed for ${install.addon.id}`);
+ }
+ };
+
+ AddonManager.addInstallListener(listener);
+ });
+}
+
+
+// add-on registers upgrade listener, and ignores update.
+add_task(function*() {
+ // discard system addon updates
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+ do_get_file("data/system_addons/system_delay_ignore.xpi").copyTo(distroDir, "system_delay_ignore@tests.mozilla.org.xpi");
+ do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+ startupManager();
+ let updateList = [
+ { id: IGNORE_ID, version: "2.0", path: "system_delay_ignore_2.xpi" },
+ { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+ ];
+
+ let postponed = promiseInstallPostponed(IGNORE_ID, NORMAL_ID);
+ yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+ yield postponed;
+
+ // addon upgrade has been delayed.
+ let addon_postponed = yield promiseAddonByID(IGNORE_ID, NORMAL_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Ignore");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+ do_check_true(Services.prefs.getBoolPref(TEST_IGNORE_PREF));
+
+ // other addons in the set are delayed as well.
+ addon_postponed = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Add-on 1");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // restarting allows upgrades to proceed
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(IGNORE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Test Delay Update Ignore");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Add-on 1");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, and allows update.
+add_task(function*() {
+ // discard system addon updates
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+ do_get_file("data/system_addons/system_delay_complete.xpi").copyTo(distroDir, "system_delay_complete@tests.mozilla.org.xpi");
+ do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+ startupManager();
+
+ let updateList = [
+ { id: COMPLETE_ID, version: "2.0", path: "system_delay_complete_2.xpi" },
+ { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+ ];
+
+ // initial state
+ let addon_allowed = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "1.0");
+ do_check_eq(addon_allowed.name, "System Test Delay Update Complete");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ addon_allowed = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "1.0");
+ do_check_eq(addon_allowed.name, "System Add-on 1");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ let resumed = promiseInstallResumed(COMPLETE_ID, NORMAL_ID);
+ yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+
+ // update is initially postponed, then resumed
+ yield resumed;
+
+ // addon upgrade has been allowed
+ addon_allowed = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Test Delay Update Complete");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // other upgrades in the set are allowed as well
+ addon_allowed = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Add-on 1");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(COMPLETE_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Test Delay Update Complete");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Add-on 1");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// add-on registers upgrade listener, initially defers update then allows upgrade
+add_task(function*() {
+ // discard system addon updates
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+ do_get_file("data/system_addons/system_delay_defer.xpi").copyTo(distroDir, "system_delay_defer@tests.mozilla.org.xpi");
+ do_get_file("data/system_addons/system1_1.xpi").copyTo(distroDir, "system1@tests.mozilla.org.xpi");
+
+ startupManager();
+
+ let updateList = [
+ { id: DEFER_ID, version: "2.0", path: "system_delay_defer_2.xpi" },
+ { id: NORMAL_ID, version: "2.0", path: "system1_2.xpi" },
+ ];
+
+ let postponed = promiseInstallPostponed(DEFER_ID, NORMAL_ID);
+ yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+ yield postponed;
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // other addons in the set are postponed as well.
+ addon_postponed = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Add-on 1");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ let deferred = promiseInstallDeferred(DEFER_ID, NORMAL_ID);
+ // add-on will not allow upgrade until fake event fires
+ AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+ yield deferred;
+
+ // addon upgrade has been allowed
+ let addon_allowed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Test Delay Update Defer");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // other addons in the set are allowed as well.
+ addon_allowed = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Add-on 1");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Test Delay Update Defer");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ addon_upgraded = yield promiseAddonByID(NORMAL_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Add-on 1");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
+
+// multiple add-ons registers upgrade listeners, initially defers then each unblock in turn.
+add_task(function*() {
+ // discard system addon updates.
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "");
+
+ do_get_file("data/system_addons/system_delay_defer.xpi").copyTo(distroDir, "system_delay_defer@tests.mozilla.org.xpi");
+ do_get_file("data/system_addons/system_delay_defer_also.xpi").copyTo(distroDir, "system_delay_defer_also@tests.mozilla.org.xpi");
+
+ startupManager();
+
+ let updateList = [
+ { id: DEFER_ID, version: "2.0", path: "system_delay_defer_2.xpi" },
+ { id: DEFER_ALSO_ID, version: "2.0", path: "system_delay_defer_also_2.xpi" },
+ ];
+
+ let postponed = promiseInstallPostponed(DEFER_ID, DEFER_ALSO_ID);
+ yield installSystemAddons(yield buildSystemAddonUpdates(updateList, root), testserver);
+ yield postponed;
+
+ // upgrade is initially postponed
+ let addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ // other addons in the set are postponed as well.
+ addon_postponed = yield promiseAddonByID(DEFER_ALSO_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Defer Also");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ let deferred = promiseInstallDeferred(DEFER_ID, DEFER_ALSO_ID);
+ // add-on will not allow upgrade until fake event fires
+ AddonManagerPrivate.callAddonListeners("onFakeEvent");
+
+ // Upgrade blockers still present.
+ addon_postponed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Defer");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ addon_postponed = yield promiseAddonByID(DEFER_ALSO_ID);
+ do_check_neq(addon_postponed, null);
+ do_check_eq(addon_postponed.version, "1.0");
+ do_check_eq(addon_postponed.name, "System Test Delay Update Defer Also");
+ do_check_true(addon_postponed.isCompatible);
+ do_check_false(addon_postponed.appDisabled);
+ do_check_true(addon_postponed.isActive);
+ do_check_eq(addon_postponed.type, "extension");
+
+ AddonManagerPrivate.callAddonListeners("onOtherFakeEvent");
+
+ yield deferred;
+
+ // addon upgrade has been allowed
+ let addon_allowed = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_allowed, null);
+ do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Test Delay Update Defer");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // other addons in the set are allowed as well.
+ addon_allowed = yield promiseAddonByID(DEFER_ALSO_ID);
+ do_check_neq(addon_allowed, null);
+ // do_check_eq(addon_allowed.version, "2.0");
+ do_check_eq(addon_allowed.name, "System Test Delay Update Defer Also");
+ do_check_true(addon_allowed.isCompatible);
+ do_check_false(addon_allowed.appDisabled);
+ do_check_true(addon_allowed.isActive);
+ do_check_eq(addon_allowed.type, "extension");
+
+ // restarting changes nothing
+ yield promiseRestartManager();
+
+ let addon_upgraded = yield promiseAddonByID(DEFER_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Test Delay Update Defer");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ addon_upgraded = yield promiseAddonByID(DEFER_ALSO_ID);
+ do_check_neq(addon_upgraded, null);
+ do_check_eq(addon_upgraded.version, "2.0");
+ do_check_eq(addon_upgraded.name, "System Test Delay Update Defer Also");
+ do_check_true(addon_upgraded.isCompatible);
+ do_check_false(addon_upgraded.appDisabled);
+ do_check_true(addon_upgraded.isActive);
+ do_check_eq(addon_upgraded.type, "extension");
+
+ yield shutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
new file mode 100644
index 000000000..31b7e5783
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_reset.js
@@ -0,0 +1,418 @@
+// Tests that we reset to the default system add-ons correctly when switching
+// application versions
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
+
+BootstrapMonitor.init();
+
+const updatesDir = FileUtils.getDir("ProfD", ["features"]);
+
+// Build the test sets
+var dir = FileUtils.getDir("ProfD", ["sysfeatures", "app1"], true);
+do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
+do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
+
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "app2"], true);
+do_get_file("data/system_addons/system1_2.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
+do_get_file("data/system_addons/system3_1.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
+
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "app3"], true);
+do_get_file("data/system_addons/system1_1_badcert.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
+do_get_file("data/system_addons/system3_1.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
+
+const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "app0"], true);
+registerDirectory("XREAppFeat", distroDir);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "0");
+
+function makeUUID() {
+ let uuidGen = AM_Cc["@mozilla.org/uuid-generator;1"].
+ getService(AM_Ci.nsIUUIDGenerator);
+ return uuidGen.generateUUID().toString();
+}
+
+function* check_installed(conditions) {
+ for (let i = 0; i < conditions.length; i++) {
+ let condition = conditions[i];
+ let id = "system" + (i + 1) + "@tests.mozilla.org";
+ let addon = yield promiseAddonByID(id);
+
+ if (!("isUpgrade" in condition) || !("version" in condition)) {
+ throw Error("condition must contain isUpgrade and version");
+ }
+ let isUpgrade = conditions[i].isUpgrade;
+ let version = conditions[i].version;
+
+ let expectedDir = isUpgrade ? updatesDir : distroDir;
+
+ if (version) {
+ // Add-on should be installed
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, version);
+ do_check_true(addon.isActive);
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.hidden);
+ do_check_true(addon.isSystem);
+ do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UPGRADE));
+ if (isUpgrade) {
+ do_check_true(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ } else {
+ do_check_false(hasFlag(addon.permissions, AddonManager.PERM_CAN_UNINSTALL));
+ }
+
+ // Verify the add-ons file is in the right place
+ let file = expectedDir.clone();
+ file.append(id + ".xpi");
+ do_check_true(file.exists());
+ do_check_true(file.isFile());
+
+ let uri = addon.getResourceURI(null);
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ do_check_eq(uri.file.path, file.path);
+
+ if (isUpgrade) {
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM);
+ }
+
+ // Verify the add-on actually started
+ BootstrapMonitor.checkAddonStarted(id, version);
+ }
+ else {
+ if (isUpgrade) {
+ // Add-on should not be installed
+ do_check_eq(addon, null);
+ }
+ else {
+ // Either add-on should not be installed or it shouldn't be active
+ do_check_true(!addon || !addon.isActive);
+ }
+
+ BootstrapMonitor.checkAddonNotStarted(id);
+
+ if (addon)
+ BootstrapMonitor.checkAddonInstalled(id);
+ else
+ BootstrapMonitor.checkAddonNotInstalled(id);
+ }
+ }
+}
+
+// Test with a missing features directory
+add_task(function* test_missing_app_dir() {
+ startupManager();
+
+ let conditions = [
+ { isUpgrade: false, version: null },
+ { isUpgrade: false, version: null },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ do_check_false(updatesDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Add some features in a new version
+add_task(function* test_new_version() {
+ gAppInfo.version = "1";
+ distroDir.leafName = "app1";
+ startupManager();
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ do_check_false(updatesDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Another new version swaps one feature and upgrades another
+add_task(function* test_upgrade() {
+ gAppInfo.version = "2";
+ distroDir.leafName = "app2";
+ startupManager();
+
+ let conditions = [
+ { isUpgrade: false, version: "2.0" },
+ { isUpgrade: false, version: null },
+ { isUpgrade: false, version: "1.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ do_check_false(updatesDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Downgrade
+add_task(function* test_downgrade() {
+ gAppInfo.version = "1";
+ distroDir.leafName = "app1";
+ startupManager();
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ do_check_false(updatesDir.exists());
+
+ yield promiseShutdownManager();
+});
+
+// Fake a mid-cycle install
+add_task(function* test_updated() {
+ // Create a random dir to install into
+ let dirname = makeUUID();
+ FileUtils.getDir("ProfD", ["features", dirname], true);
+ updatesDir.append(dirname);
+
+ // Copy in the system add-ons
+ let file = do_get_file("data/system_addons/system2_2.xpi");
+ file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi");
+ file = do_get_file("data/system_addons/system3_2.xpi");
+ file.copyTo(updatesDir, "system3@tests.mozilla.org.xpi");
+
+ // Inject it into the system set
+ let addonSet = {
+ schema: 1,
+ directory: updatesDir.leafName,
+ addons: {
+ "system2@tests.mozilla.org": {
+ version: "2.0"
+ },
+ "system3@tests.mozilla.org": {
+ version: "2.0"
+ },
+ }
+ };
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: true, version: "2.0" },
+ { isUpgrade: true, version: "2.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Entering safe mode should disable the updated system add-ons and use the
+// default system add-ons
+add_task(function* safe_mode_disabled() {
+ gAppInfo.inSafeMode = true;
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Leaving safe mode should re-enable the updated system add-ons
+add_task(function* normal_mode_enabled() {
+ gAppInfo.inSafeMode = false;
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: true, version: "2.0" },
+ { isUpgrade: true, version: "2.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// An additional add-on in the directory should be ignored
+add_task(function* test_skips_additional() {
+ // Copy in the system add-ons
+ let file = do_get_file("data/system_addons/system4_1.xpi");
+ file.copyTo(updatesDir, "system4@tests.mozilla.org.xpi");
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: true, version: "2.0" },
+ { isUpgrade: true, version: "2.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Missing add-on should revert to the default set
+add_task(function* test_revert() {
+ manuallyUninstall(updatesDir, "system2@tests.mozilla.org");
+
+ // With the add-on physically gone from disk we won't see uninstall events
+ BootstrapMonitor.clear("system2@tests.mozilla.org");
+
+ startupManager(false);
+
+ // With system add-on 2 gone the updated set is now invalid so it reverts to
+ // the default set which is system add-ons 1 and 2.
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Putting it back will make the set work again
+add_task(function* test_reuse() {
+ let file = do_get_file("data/system_addons/system2_2.xpi");
+ file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi");
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: true, version: "2.0" },
+ { isUpgrade: true, version: "2.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Making the pref corrupt should revert to the default set
+add_task(function* test_corrupt_pref() {
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, "foo");
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// An add-on with a bad certificate should cause us to use the default set
+add_task(function* test_bad_profile_cert() {
+ let file = do_get_file("data/system_addons/system1_1_badcert.xpi");
+ file.copyTo(updatesDir, "system1@tests.mozilla.org.xpi");
+
+ // Inject it into the system set
+ let addonSet = {
+ schema: 1,
+ directory: updatesDir.leafName,
+ addons: {
+ "system1@tests.mozilla.org": {
+ version: "2.0"
+ },
+ "system2@tests.mozilla.org": {
+ version: "1.0"
+ },
+ "system3@tests.mozilla.org": {
+ version: "1.0"
+ },
+ }
+ };
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// Switching to app defaults that contain a bad certificate should still work
+add_task(function* test_bad_app_cert() {
+ gAppInfo.version = "3";
+ distroDir.leafName = "app3";
+ startupManager();
+
+ // Add-on will still be present
+ let addon = yield promiseAddonByID("system1@tests.mozilla.org");
+ do_check_neq(addon, null);
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ { isUpgrade: false, version: null },
+ { isUpgrade: false, version: "1.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
+
+// A failed upgrade should revert to the default set.
+add_task(function* test_updated() {
+ // Create a random dir to install into
+ let dirname = makeUUID();
+ FileUtils.getDir("ProfD", ["features", dirname], true);
+ updatesDir.append(dirname);
+
+ // Copy in the system add-ons
+ let file = do_get_file("data/system_addons/system2_2.xpi");
+ file.copyTo(updatesDir, "system2@tests.mozilla.org.xpi");
+ file = do_get_file("data/system_addons/system_failed_update.xpi");
+ file.copyTo(updatesDir, "system_failed_update@tests.mozilla.org.xpi");
+
+ // Inject it into the system set
+ let addonSet = {
+ schema: 1,
+ directory: updatesDir.leafName,
+ addons: {
+ "system2@tests.mozilla.org": {
+ version: "2.0"
+ },
+ "system_failed_update@tests.mozilla.org": {
+ version: "1.0"
+ },
+ }
+ };
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify(addonSet));
+
+ startupManager(false);
+
+ let conditions = [
+ { isUpgrade: false, version: "1.0" },
+ ];
+
+ yield check_installed(conditions);
+
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
new file mode 100644
index 000000000..c8e314427
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_system_update.js
@@ -0,0 +1,788 @@
+// Tests that we reset to the default system add-ons correctly when switching
+// application versions
+const PREF_SYSTEM_ADDON_SET = "extensions.systemAddonSet";
+const PREF_SYSTEM_ADDON_UPDATE_URL = "extensions.systemAddon.update.url";
+const PREF_XPI_STATE = "extensions.xpiState";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+
+Components.utils.import("resource://testing-common/httpd.js");
+const { computeHash } = Components.utils.import("resource://gre/modules/addons/ProductAddonChecker.jsm");
+
+BootstrapMonitor.init();
+
+const updatesDir = FileUtils.getDir("ProfD", ["features"], false);
+
+function getCurrentUpdatesDir() {
+ let dir = updatesDir.clone();
+ let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
+ dir.append(set.directory);
+ return dir;
+}
+
+function clearUpdatesDir() {
+ // Delete any existing directories
+ if (updatesDir.exists())
+ updatesDir.remove(true);
+
+ Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
+}
+
+function buildPrefilledUpdatesDir() {
+ clearUpdatesDir();
+
+ // Build the test set
+ let dir = FileUtils.getDir("ProfD", ["features", "prefilled"], true);
+
+ do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
+ do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
+
+ // Mark these in the past so the startup file scan notices when files have changed properly
+ FileUtils.getFile("ProfD", ["features", "prefilled", "system2@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
+ FileUtils.getFile("ProfD", ["features", "prefilled", "system3@tests.mozilla.org.xpi"]).lastModifiedTime -= 10000;
+
+ Services.prefs.setCharPref(PREF_SYSTEM_ADDON_SET, JSON.stringify({
+ schema: 1,
+ directory: dir.leafName,
+ addons: {
+ "system2@tests.mozilla.org": {
+ version: "2.0"
+ },
+ "system3@tests.mozilla.org": {
+ version: "2.0"
+ },
+ }
+ }));
+}
+
+let dir = FileUtils.getDir("ProfD", ["sysfeatures", "hidden"], true);
+do_get_file("data/system_addons/system1_1.xpi").copyTo(dir, "system1@tests.mozilla.org.xpi");
+do_get_file("data/system_addons/system2_1.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
+
+dir = FileUtils.getDir("ProfD", ["sysfeatures", "prefilled"], true);
+do_get_file("data/system_addons/system2_2.xpi").copyTo(dir, "system2@tests.mozilla.org.xpi");
+do_get_file("data/system_addons/system3_2.xpi").copyTo(dir, "system3@tests.mozilla.org.xpi");
+
+const distroDir = FileUtils.getDir("ProfD", ["sysfeatures", "empty"], true);
+registerDirectory("XREAppFeat", distroDir);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "2");
+
+var testserver = new HttpServer();
+testserver.registerDirectory("/data/", do_get_file("data/system_addons"));
+testserver.start();
+var root = testserver.identity.primaryScheme + "://" +
+ testserver.identity.primaryHost + ":" +
+ testserver.identity.primaryPort + "/data/"
+Services.prefs.setCharPref(PREF_SYSTEM_ADDON_UPDATE_URL, root + "update.xml");
+
+function makeUUID() {
+ let uuidGen = AM_Cc["@mozilla.org/uuid-generator;1"].
+ getService(AM_Ci.nsIUUIDGenerator);
+ return uuidGen.generateUUID().toString();
+}
+
+function* check_installed(conditions) {
+ for (let i = 0; i < conditions.length; i++) {
+ let condition = conditions[i];
+ let id = "system" + (i + 1) + "@tests.mozilla.org";
+ let addon = yield promiseAddonByID(id);
+
+ if (!("isUpgrade" in condition) || !("version" in condition)) {
+ throw Error("condition must contain isUpgrade and version");
+ }
+ let isUpgrade = conditions[i].isUpgrade;
+ let version = conditions[i].version;
+
+ let expectedDir = isUpgrade ? getCurrentUpdatesDir() : distroDir;
+
+ if (version) {
+ do_print(`Checking state of add-on ${id}, expecting version ${version}`);
+
+ // Add-on should be installed
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, version);
+ do_check_true(addon.isActive);
+ do_check_false(addon.foreignInstall);
+ do_check_true(addon.hidden);
+ do_check_true(addon.isSystem);
+
+ // Verify the add-ons file is in the right place
+ let file = expectedDir.clone();
+ file.append(id + ".xpi");
+ do_check_true(file.exists());
+ do_check_true(file.isFile());
+
+ let uri = addon.getResourceURI(null);
+ do_check_true(uri instanceof AM_Ci.nsIFileURL);
+ do_check_eq(uri.file.path, file.path);
+
+ if (isUpgrade) {
+ do_check_eq(addon.signedState, AddonManager.SIGNEDSTATE_SYSTEM);
+ }
+
+ // Verify the add-on actually started
+ BootstrapMonitor.checkAddonStarted(id, version);
+ }
+ else {
+ do_print(`Checking state of add-on ${id}, expecting it to be missing`);
+
+ if (isUpgrade) {
+ // Add-on should not be installed
+ do_check_eq(addon, null);
+ }
+
+ BootstrapMonitor.checkAddonNotStarted(id);
+
+ if (addon)
+ BootstrapMonitor.checkAddonInstalled(id);
+ else
+ BootstrapMonitor.checkAddonNotInstalled(id);
+ }
+ }
+}
+
+
+/**
+ * Defines the set of initial conditions to run each test against. Each should
+ * define the following properties:
+ *
+ * setup: A task to setup the profile into the initial state.
+ * initialState: The initial expected system add-on state after setup has run.
+ */
+const TEST_CONDITIONS = {
+ // Runs tests with no updated or default system add-ons initially installed
+ blank: {
+ setup: function*() {
+ clearUpdatesDir();
+ distroDir.leafName = "empty";
+ },
+ initialState: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ },
+ // Runs tests with default system add-ons installed
+ withAppSet: {
+ setup: function*() {
+ clearUpdatesDir();
+ distroDir.leafName = "prefilled";
+ },
+ initialState: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ]
+ },
+
+ // Runs tests with updated system add-ons installed
+ withProfileSet: {
+ setup: function*() {
+ buildPrefilledUpdatesDir();
+ distroDir.leafName = "empty";
+ },
+ initialState: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ]
+ },
+
+ // Runs tests with both default and updated system add-ons installed
+ withBothSets: {
+ setup: function*() {
+ buildPrefilledUpdatesDir();
+ distroDir.leafName = "hidden";
+ },
+ initialState: [
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ]
+ },
+};
+
+
+/**
+ * The tests to run. Each test must define an updateList or test. The following
+ * properties are used:
+ *
+ * updateList: The set of add-ons the server should respond with.
+ * test: A function to run to perform the update check (replaces
+ * updateList)
+ * fails: An optional property, if true the update check is expected to
+ * fail.
+ * finalState: An optional property, the expected final state of system add-ons,
+ * if missing the test condition's initialState is used.
+ */
+const TESTS = {
+ // Test that a blank response does nothing
+ blank: {
+ updateList: null,
+ },
+
+ // Test that an empty list removes existing updates, leaving defaults.
+ empty: {
+ updateList: [],
+ finalState: {
+ blank: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withAppSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withProfileSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withBothSets: [
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ // Set this to `true` to so `verify_state()` expects a blank profile dir
+ { isUpgrade: true, version: null}
+ ]
+ },
+ },
+ // Tests that a new set of system add-ons gets installed
+ newset: {
+ updateList: [
+ { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" },
+ { id: "system5@tests.mozilla.org", version: "1.0", path: "system5_1.xpi" }
+ ],
+ finalState: {
+ blank: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withAppSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: false, version: "2.0"},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withProfileSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withBothSets: [
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: true, version: "1.0"}
+ ]
+ }
+ },
+
+ // Tests that an upgraded set of system add-ons gets installed
+ upgrades: {
+ updateList: [
+ { id: "system2@tests.mozilla.org", version: "3.0", path: "system2_3.xpi" },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi" }
+ ],
+ finalState: {
+ blank: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withAppSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withProfileSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ],
+ withBothSets: [
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: false, version: null}
+ ]
+ }
+ },
+
+ // Tests that a set of system add-ons, some new, some existing gets installed
+ overlapping: {
+ updateList: [
+ { id: "system1@tests.mozilla.org", version: "2.0", path: "system1_2.xpi" },
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi" },
+ { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
+ ],
+ finalState: {
+ blank: [
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: false, version: null}
+ ],
+ withAppSet: [
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: false, version: null}
+ ],
+ withProfileSet: [
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: false, version: null}
+ ],
+ withBothSets: [
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "2.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "1.0"},
+ { isUpgrade: false, version: null}
+ ]
+ }
+ },
+
+ // Specifying an incorrect version should stop us updating anything
+ badVersion: {
+ fails: true,
+ updateList: [
+ { id: "system2@tests.mozilla.org", version: "4.0", path: "system2_3.xpi" },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi" }
+ ],
+ },
+
+ // Specifying an invalid size should stop us updating anything
+ badSize: {
+ fails: true,
+ updateList: [
+ { id: "system2@tests.mozilla.org", version: "3.0", path: "system2_3.xpi", size: 2 },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi" }
+ ],
+ },
+
+ // Specifying an incorrect hash should stop us updating anything
+ badHash: {
+ fails: true,
+ updateList: [
+ { id: "system2@tests.mozilla.org", version: "3.0", path: "system2_3.xpi" },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi", hashFunction: "sha1", hashValue: "205a4c49bd513ebd30594e380c19e86bba1f83e2" }
+ ],
+ },
+
+ // Correct sizes and hashes should work
+ checkSizeHash: {
+ updateList: [
+ { id: "system2@tests.mozilla.org", version: "3.0", path: "system2_3.xpi", size: 4697 },
+ { id: "system3@tests.mozilla.org", version: "3.0", path: "system3_3.xpi", hashFunction: "sha1", hashValue: "a4c7198d56deb315511c02937fd96c696de6cb84" },
+ { id: "system5@tests.mozilla.org", version: "1.0", path: "system5_1.xpi", size: 4691, hashFunction: "sha1", hashValue: "6887b916a1a9a5338b0df4181f6187f5396861eb" }
+ ],
+ finalState: {
+ blank: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withAppSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withProfileSet: [
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"}
+ ],
+ withBothSets: [
+ { isUpgrade: false, version: "1.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: true, version: "3.0"},
+ { isUpgrade: false, version: null},
+ { isUpgrade: true, version: "1.0"}
+ ]
+ }
+ },
+
+ // A bad certificate should stop updates
+ badCert: {
+ fails: true,
+ updateList: [
+ { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1_badcert.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ],
+ },
+
+ // An unpacked add-on should stop updates.
+ notPacked: {
+ fails: true,
+ updateList: [
+ { id: "system6@tests.mozilla.org", version: "1.0", path: "system6_1_unpack.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ],
+ },
+
+ // A non-bootstrap add-on should stop updates.
+ notBootstrap: {
+ fails: true,
+ updateList: [
+ { id: "system6@tests.mozilla.org", version: "1.0", path: "system6_2_notBootstrap.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ],
+ },
+
+ // A non-multiprocess add-on should stop updates.
+ notMultiprocess: {
+ fails: true,
+ updateList: [
+ { id: "system6@tests.mozilla.org", version: "1.0", path: "system6_3_notMultiprocess.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ],
+ }
+}
+
+add_task(function* setup() {
+ // Initialise the profile
+ startupManager();
+ yield promiseShutdownManager();
+})
+
+function* get_directories() {
+ let subdirs = [];
+
+ if (yield OS.File.exists(updatesDir.path)) {
+ let iterator = new OS.File.DirectoryIterator(updatesDir.path);
+ yield iterator.forEach(entry => {
+ if (entry.isDir) {
+ subdirs.push(entry);
+ }
+ });
+ iterator.close();
+ }
+
+ return subdirs;
+}
+
+function* setup_conditions(setup) {
+ do_print("Clearing existing database.");
+ Services.prefs.clearUserPref(PREF_SYSTEM_ADDON_SET);
+ distroDir.leafName = "empty";
+ startupManager(false);
+ yield promiseShutdownManager();
+
+ do_print("Setting up conditions.");
+ yield setup.setup();
+
+ startupManager(false);
+
+ // Make sure the initial state is correct
+ do_print("Checking initial state.");
+ yield check_installed(setup.initialState);
+}
+
+function* verify_state(initialState, finalState = undefined, alreadyUpgraded = false) {
+ let expectedDirs = 0;
+
+ // If the initial state was using the profile set then that directory will
+ // still exist.
+
+ if (initialState.some(a => a.isUpgrade)) {
+ expectedDirs++;
+ }
+
+ if (finalState == undefined) {
+ finalState = initialState;
+ }
+ else if (finalState.some(a => a.isUpgrade)) {
+ // If the new state is using the profile then that directory will exist.
+ expectedDirs++;
+ }
+
+ // Since upgrades are restartless now, the previous update dir hasn't been removed.
+ if (alreadyUpgraded) {
+ expectedDirs++;
+ }
+
+ do_print("Checking final state.");
+
+ let dirs = yield get_directories();
+ do_check_eq(dirs.length, expectedDirs);
+
+ yield check_installed(...finalState);
+
+ // Check that the new state is active after a restart
+ yield promiseRestartManager();
+ yield check_installed(finalState);
+}
+
+function* exec_test(setupName, testName) {
+ let setup = TEST_CONDITIONS[setupName];
+ let test = TESTS[testName];
+
+ yield setup_conditions(setup);
+
+ try {
+ if ("test" in test) {
+ yield test.test();
+ }
+ else {
+ yield installSystemAddons(yield buildSystemAddonUpdates(test.updateList, root), testserver);
+ }
+
+ if (test.fails) {
+ do_throw("Expected this test to fail");
+ }
+ }
+ catch (e) {
+ if (!test.fails) {
+ do_throw(e);
+ }
+ }
+
+ // some tests have a different expected combination of default
+ // and updated add-ons.
+ if (test.finalState && setupName in test.finalState) {
+ yield verify_state(setup.initialState, test.finalState[setupName]);
+ }
+ else {
+ yield verify_state(setup.initialState, test.finalState);
+ }
+
+ yield promiseShutdownManager();
+}
+
+add_task(function*() {
+ for (let setup of Object.keys(TEST_CONDITIONS)) {
+ for (let test of Object.keys(TESTS)) {
+ do_print("Running test " + setup + " " + test);
+
+ yield exec_test(setup, test);
+ }
+ }
+});
+
+// Some custom tests
+// Test that the update check is performed as part of the regular add-on update
+// check
+add_task(function* test_addon_update() {
+ yield setup_conditions(TEST_CONDITIONS.blank);
+
+ yield updateAllSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+ ], root), testserver);
+
+ yield verify_state(TEST_CONDITIONS.blank.initialState, [
+ {isUpgrade: false, version: null},
+ {isUpgrade: true, version: "2.0"},
+ {isUpgrade: true, version: "2.0"},
+ {isUpgrade: false, version: null},
+ {isUpgrade: false, version: null}
+ ]);
+
+ yield promiseShutdownManager();
+});
+
+// Disabling app updates should block system add-on updates
+add_task(function* test_app_update_disabled() {
+ yield setup_conditions(TEST_CONDITIONS.blank);
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+ yield updateAllSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+ ], root), testserver);
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
+
+ yield verify_state(TEST_CONDITIONS.blank.initialState);
+
+ yield promiseShutdownManager();
+});
+
+// Safe mode should block system add-on updates
+add_task(function* test_safe_mode() {
+ gAppInfo.inSafeMode = true;
+
+ yield setup_conditions(TEST_CONDITIONS.blank);
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+ yield updateAllSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+ ], root), testserver);
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
+
+ yield verify_state(TEST_CONDITIONS.blank.initialState);
+
+ yield promiseShutdownManager();
+
+ gAppInfo.inSafeMode = false;
+});
+
+// Tests that a set that matches the default set does nothing
+add_task(function* test_match_default() {
+ yield setup_conditions(TEST_CONDITIONS.withAppSet);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+ ], root), testserver);
+
+ // Shouldn't have installed an updated set
+ yield verify_state(TEST_CONDITIONS.withAppSet.initialState);
+
+ yield promiseShutdownManager();
+});
+
+// Tests that a set that matches the hidden default set works
+add_task(function* test_match_default_revert() {
+ yield setup_conditions(TEST_CONDITIONS.withBothSets);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system1@tests.mozilla.org", version: "1.0", path: "system1_1.xpi" },
+ { id: "system2@tests.mozilla.org", version: "1.0", path: "system2_1.xpi" }
+ ], root), testserver);
+
+ // This should revert to the default set instead of installing new versions
+ // into an updated set.
+ yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
+ {isUpgrade: false, version: "1.0"},
+ {isUpgrade: false, version: "1.0"},
+ {isUpgrade: false, version: null},
+ {isUpgrade: false, version: null},
+ {isUpgrade: false, version: null}
+ ]);
+
+ yield promiseShutdownManager();
+});
+
+// Tests that a set that matches the current set works
+add_task(function* test_match_current() {
+ yield setup_conditions(TEST_CONDITIONS.withBothSets);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" }
+ ], root), testserver);
+
+ // This should remain with the current set instead of creating a new copy
+ let set = JSON.parse(Services.prefs.getCharPref(PREF_SYSTEM_ADDON_SET));
+ do_check_eq(set.directory, "prefilled");
+
+ yield verify_state(TEST_CONDITIONS.withBothSets.initialState);
+
+ yield promiseShutdownManager();
+});
+
+// Tests that a set with a minor change doesn't re-download existing files
+add_task(function* test_no_download() {
+ yield setup_conditions(TEST_CONDITIONS.withBothSets);
+
+ // The missing file here is unneeded since there is a local version already
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "missing.xpi" },
+ { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
+ ], root), testserver);
+
+ yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
+ {isUpgrade: false, version: "1.0"},
+ {isUpgrade: true, version: "2.0"},
+ {isUpgrade: false, version: null},
+ {isUpgrade: true, version: "1.0"},
+ {isUpgrade: false, version: null}
+ ]);
+
+ yield promiseShutdownManager();
+});
+
+// Tests that a second update before a restart works
+add_task(function* test_double_update() {
+ yield setup_conditions(TEST_CONDITIONS.withAppSet);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ], root), testserver);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system3@tests.mozilla.org", version: "2.0", path: "system3_2.xpi" },
+ { id: "system4@tests.mozilla.org", version: "1.0", path: "system4_1.xpi" }
+ ], root), testserver);
+
+ yield verify_state(TEST_CONDITIONS.withAppSet.initialState, [
+ {isUpgrade: false, version: null},
+ {isUpgrade: false, version: "2.0"},
+ {isUpgrade: true, version: "2.0"},
+ {isUpgrade: true, version: "1.0"},
+ {isUpgrade: false, version: null}
+ ], true);
+
+ yield promiseShutdownManager();
+});
+
+// A second update after a restart will delete the original unused set
+add_task(function* test_update_purges() {
+ yield setup_conditions(TEST_CONDITIONS.withBothSets);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates([
+ { id: "system2@tests.mozilla.org", version: "2.0", path: "system2_2.xpi" },
+ { id: "system3@tests.mozilla.org", version: "1.0", path: "system3_1.xpi" }
+ ], root), testserver);
+
+ yield verify_state(TEST_CONDITIONS.withBothSets.initialState, [
+ {isUpgrade: false, version: "1.0"},
+ {isUpgrade: true, version: "2.0"},
+ {isUpgrade: true, version: "1.0"},
+ {isUpgrade: false, version: null},
+ {isUpgrade: false, version: null}
+ ]);
+
+ yield installSystemAddons(yield buildSystemAddonUpdates(null), testserver);
+
+ let dirs = yield get_directories();
+ do_check_eq(dirs.length, 1);
+
+ yield promiseShutdownManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js b/toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js
new file mode 100644
index 000000000..ef4f2aee5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_targetPlatforms.js
@@ -0,0 +1,146 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that the targetPlatform entries are checked when deciding
+// if an add-on is incompatible.
+
+// No targetPlatforms so should be compatible
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Matches the OS
+var addon2 = {
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 2",
+ targetPlatforms: [
+ "XPCShell",
+ "WINNT_x86",
+ "XPCShell"
+ ],
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Matches the OS and ABI
+var addon3 = {
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ targetPlatforms: [
+ "WINNT",
+ "XPCShell_noarch-spidermonkey"
+ ],
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Doesn't match
+var addon4 = {
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 4",
+ targetPlatforms: [
+ "WINNT_noarch-spidermonkey",
+ "Darwin",
+ "WINNT_noarch-spidermonkey"
+ ],
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+// Matches the OS but since a different entry specifies ABI this doesn't match.
+var addon5 = {
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 5",
+ targetPlatforms: [
+ "XPCShell",
+ "XPCShell_foo"
+ ],
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Set up the profile
+function run_test() {
+ do_test_pending();
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ writeInstallRDFForExtension(addon2, profileDir);
+ writeInstallRDFForExtension(addon3, profileDir);
+ writeInstallRDFForExtension(addon4, profileDir);
+ writeInstallRDFForExtension(addon5, profileDir);
+
+ restartManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org"],
+ function([a1, a2, a3, a4, a5]) {
+
+ do_check_neq(a1, null);
+ do_check_false(a1.appDisabled);
+ do_check_true(a1.isPlatformCompatible);
+ do_check_true(a1.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ do_check_neq(a2, null);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isPlatformCompatible);
+ do_check_true(a2.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+ do_check_in_crash_annotation(addon2.id, addon2.version);
+
+ do_check_neq(a3, null);
+ do_check_false(a3.appDisabled);
+ do_check_true(a3.isPlatformCompatible);
+ do_check_true(a3.isActive);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+ do_check_in_crash_annotation(addon3.id, addon3.version);
+
+ do_check_neq(a4, null);
+ do_check_true(a4.appDisabled);
+ do_check_false(a4.isPlatformCompatible);
+ do_check_false(a4.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a4.id));
+ do_check_not_in_crash_annotation(addon4.id, addon4.version);
+
+ do_check_neq(a5, null);
+ do_check_true(a5.appDisabled);
+ do_check_false(a5.isPlatformCompatible);
+ do_check_false(a5.isActive);
+ do_check_false(isExtensionInAddonsList(profileDir, a5.id));
+ do_check_not_in_crash_annotation(addon5.id, addon5.version);
+
+ do_execute_soon(do_test_finished);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
new file mode 100644
index 000000000..ec9e25a0b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -0,0 +1,760 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "bootstrap1@tests.mozilla.org";
+const sampleRDFManifest = {
+ id: ID,
+ version: "1.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (temporary)",
+};
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+const {Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseAddonStartup() {
+ return new Promise(resolve => {
+ let listener = (extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+BootstrapMonitor.init();
+
+// Partial list of bootstrap reasons from XPIProvider.jsm
+const BOOTSTRAP_REASONS = {
+ ADDON_INSTALL: 5,
+ ADDON_UPGRADE: 7,
+ ADDON_DOWNGRADE: 8,
+};
+
+function waitForBootstrapEvent(expectedEvent, addonId) {
+ return new Promise(resolve => {
+ const observer = {
+ observe: (subject, topic, data) => {
+ const info = JSON.parse(data);
+ const targetAddonId = info.data.id;
+ if (targetAddonId === addonId && info.event === expectedEvent) {
+ resolve(info);
+ Services.obs.removeObserver(observer);
+ } else {
+ do_print(
+ `Ignoring bootstrap event: ${info.event} for ${targetAddonId}`);
+ }
+ },
+ };
+ Services.obs.addObserver(observer, "bootstrapmonitor-event", false);
+ });
+}
+
+// Install a temporary add-on with no existing add-on present.
+// Restart and make sure it has gone away.
+add_task(function*() {
+ let extInstallCalled = false;
+ AddonManager.addInstallListener({
+ onExternalInstall: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ extInstallCalled = true;
+ },
+ });
+
+ let installingCalled = false;
+ let installedCalled = false;
+ AddonManager.addAddonListener({
+ onInstalling: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installingCalled = true;
+ },
+ onInstalled: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "1.0");
+ installedCalled = true;
+ },
+ onInstallStarted: (aInstall) => {
+ do_throw("onInstallStarted called unexpectedly");
+ }
+ });
+
+ yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
+
+ do_check_true(extInstallCalled);
+ do_check_true(installingCalled);
+ do_check_true(installedCalled);
+
+ const install = BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ yield promiseRestartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ yield promiseRestartManager();
+});
+
+// Install a temporary add-on over the top of an existing add-on.
+// Restart and make sure the existing add-on comes back.
+add_task(function*() {
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let tempdir = gTmpD.clone();
+
+ // test that an unpacked add-on works too
+ writeInstallRDFToDir({
+ id: ID,
+ version: "3.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (temporary)",
+ }, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
+
+ let unpacked_addon = tempdir.clone();
+ unpacked_addon.append(ID);
+ do_get_file("data/test_temporary/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+
+ yield AddonManager.installTemporaryAddon(unpacked_addon);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "3.0");
+ BootstrapMonitor.checkAddonStarted(ID, "3.0");
+
+ addon = yield promiseAddonByID(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "3.0");
+ do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+
+ // existing add-on is back
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ unpacked_addon.remove(true);
+
+ // on Windows XPI files will be locked by the JAR cache, skip this test there.
+ if (!("nsIWindowsRegKey" in Components.interfaces)) {
+ // test that a packed (XPI) add-on works
+ writeInstallRDFToXPI({
+ id: ID,
+ version: "2.0",
+ bootstrap: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (temporary)",
+ }, tempdir, "bootstrap1@tests.mozilla.org");
+
+ let packed_addon = tempdir.clone();
+ packed_addon.append(ID + ".xpi");
+
+ yield AddonManager.installTemporaryAddon(packed_addon);
+
+ addon = yield promiseAddonByID(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "2.0");
+ do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+
+ // existing add-on is back
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ packed_addon.remove(false);
+
+ // test that a webextension works
+ let webext = createTempWebExtensionFile({
+ manifest: {
+ version: "4.0",
+ name: "Test WebExtension 1 (temporary)",
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }
+ });
+
+ yield Promise.all([
+ AddonManager.installTemporaryAddon(webext),
+ promiseAddonStartup(),
+ ]);
+ addon = yield promiseAddonByID(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "4.0");
+ do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ // test that re-loading a webextension works, using the same filename
+ webext.remove(false);
+ webext = createTempWebExtensionFile({
+ manifest: {
+ version: "5.0",
+ name: "Test WebExtension 1 (temporary)",
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }
+ });
+
+ yield Promise.all([
+ AddonManager.installTemporaryAddon(webext),
+ promiseAddonStartup(),
+ ]);
+ addon = yield promiseAddonByID(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "5.0");
+ do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+
+ // existing add-on is back
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+ }
+
+ // remove original add-on
+ addon.uninstall();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseRestartManager();
+});
+
+// Install a temporary add-on over the top of an existing add-on.
+// Uninstall it and make sure the existing add-on comes back.
+add_task(function*() {
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let tempdir = gTmpD.clone();
+ writeInstallRDFToDir({
+ id: ID,
+ version: "2.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (temporary)",
+ }, tempdir);
+
+ let unpacked_addon = tempdir.clone();
+ unpacked_addon.append(ID);
+
+ let extInstallCalled = false;
+ AddonManager.addInstallListener({
+ onExternalInstall: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "2.0");
+ extInstallCalled = true;
+ },
+ });
+
+ let installingCalled = false;
+ let installedCalled = false;
+ AddonManager.addAddonListener({
+ onInstalling: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ if (!installingCalled)
+ do_check_eq(aInstall.version, "2.0");
+ installingCalled = true;
+ },
+ onInstalled: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ if (!installedCalled)
+ do_check_eq(aInstall.version, "2.0");
+ installedCalled = true;
+ },
+ onInstallStarted: (aInstall) => {
+ do_throw("onInstallStarted called unexpectedly");
+ }
+ });
+
+ yield AddonManager.installTemporaryAddon(unpacked_addon);
+
+ do_check_true(extInstallCalled);
+ do_check_true(installingCalled);
+ do_check_true(installedCalled);
+
+ let addon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "2.0");
+ do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ addon.uninstall();
+
+ addon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ // existing add-on is back
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ unpacked_addon.remove(true);
+ addon.uninstall();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseRestartManager();
+});
+
+// Install a temporary add-on as a version upgrade over the top of an
+// existing temporary add-on.
+add_task(function*() {
+ const tempdir = gTmpD.clone();
+
+ writeInstallRDFToDir(sampleRDFManifest, tempdir,
+ "bootstrap1@tests.mozilla.org", "bootstrap.js");
+
+ const unpackedAddon = tempdir.clone();
+ unpackedAddon.append(ID);
+ do_get_file("data/test_temporary/bootstrap.js")
+ .copyTo(unpackedAddon, "bootstrap.js");
+
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ // Increment the version number, re-install it, and make sure it
+ // gets marked as an upgrade.
+ writeInstallRDFToDir(Object.assign({}, sampleRDFManifest, {
+ version: "2.0"
+ }), tempdir, "bootstrap1@tests.mozilla.org");
+
+ const onUninstall = waitForBootstrapEvent("uninstall", ID);
+ const onInstall = waitForBootstrapEvent("install", ID);
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ const uninstall = yield onUninstall;
+ equal(uninstall.data.version, "1.0");
+ equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_UPGRADE);
+
+ const install = yield onInstall;
+ equal(install.data.version, "2.0");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_UPGRADE);
+
+ const addon = yield promiseAddonByID(ID);
+ addon.uninstall();
+
+ unpackedAddon.remove(true);
+ yield promiseRestartManager();
+});
+
+// Install a temporary add-on as a version downgrade over the top of an
+// existing temporary add-on.
+add_task(function*() {
+ const tempdir = gTmpD.clone();
+
+ writeInstallRDFToDir(sampleRDFManifest, tempdir,
+ "bootstrap1@tests.mozilla.org", "bootstrap.js");
+
+ const unpackedAddon = tempdir.clone();
+ unpackedAddon.append(ID);
+ do_get_file("data/test_temporary/bootstrap.js")
+ .copyTo(unpackedAddon, "bootstrap.js");
+
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ // Decrement the version number, re-install, and make sure
+ // it gets marked as a downgrade.
+ writeInstallRDFToDir(Object.assign({}, sampleRDFManifest, {
+ version: "0.8"
+ }), tempdir, "bootstrap1@tests.mozilla.org");
+
+ const onUninstall = waitForBootstrapEvent("uninstall", ID);
+ const onInstall = waitForBootstrapEvent("install", ID);
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ const uninstall = yield onUninstall;
+ equal(uninstall.data.version, "1.0");
+ equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_DOWNGRADE);
+
+ const install = yield onInstall;
+ equal(install.data.version, "0.8");
+ equal(install.reason, BOOTSTRAP_REASONS.ADDON_DOWNGRADE);
+
+ const addon = yield promiseAddonByID(ID);
+ addon.uninstall();
+
+ unpackedAddon.remove(true);
+ yield promiseRestartManager();
+});
+
+// Installing a temporary add-on over an existing add-on with the same
+// version number should be installed as an upgrade.
+add_task(function*() {
+ const tempdir = gTmpD.clone();
+
+ writeInstallRDFToDir(sampleRDFManifest, tempdir,
+ "bootstrap1@tests.mozilla.org", "bootstrap.js");
+
+ const unpackedAddon = tempdir.clone();
+ unpackedAddon.append(ID);
+ do_get_file("data/test_temporary/bootstrap.js")
+ .copyTo(unpackedAddon, "bootstrap.js");
+
+ const onInitialInstall = waitForBootstrapEvent("install", ID);
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ const initialInstall = yield onInitialInstall;
+ equal(initialInstall.data.version, "1.0");
+ equal(initialInstall.reason, BOOTSTRAP_REASONS.ADDON_INSTALL);
+
+ // Install it again.
+ const onUninstall = waitForBootstrapEvent("uninstall", ID);
+ const onInstall = waitForBootstrapEvent("install", ID);
+ yield AddonManager.installTemporaryAddon(unpackedAddon);
+
+ const uninstall = yield onUninstall;
+ equal(uninstall.data.version, "1.0");
+ equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_UPGRADE);
+
+ const reInstall = yield onInstall;
+ equal(reInstall.data.version, "1.0");
+ equal(reInstall.reason, BOOTSTRAP_REASONS.ADDON_UPGRADE);
+
+ const addon = yield promiseAddonByID(ID);
+ addon.uninstall();
+
+ unpackedAddon.remove(true);
+ yield promiseRestartManager();
+});
+
+// Install a temporary add-on over the top of an existing disabled add-on.
+// After restart, the existing add-on should continue to be installed and disabled.
+add_task(function*() {
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let addon = yield promiseAddonByID(ID);
+
+ addon.userDisabled = true;
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ let tempdir = gTmpD.clone();
+ writeInstallRDFToDir({
+ id: ID,
+ version: "2.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Bootstrap 1 (temporary)",
+ }, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
+
+ let unpacked_addon = tempdir.clone();
+ unpacked_addon.append(ID);
+ do_get_file("data/test_temporary/bootstrap.js")
+ .copyTo(unpacked_addon, "bootstrap.js");
+
+ let extInstallCalled = false;
+ AddonManager.addInstallListener({
+ onExternalInstall: (aInstall) => {
+ do_check_eq(aInstall.id, ID);
+ do_check_eq(aInstall.version, "2.0");
+ extInstallCalled = true;
+ },
+ });
+
+ yield AddonManager.installTemporaryAddon(unpacked_addon);
+
+ do_check_true(extInstallCalled);
+
+ let tempAddon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "2.0");
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ // temporary add-on is installed and started
+ do_check_neq(tempAddon, null);
+ do_check_eq(tempAddon.version, "2.0");
+ do_check_eq(tempAddon.name, "Test Bootstrap 1 (temporary)");
+ do_check_true(tempAddon.isCompatible);
+ do_check_false(tempAddon.appDisabled);
+ do_check_true(tempAddon.isActive);
+ do_check_eq(tempAddon.type, "extension");
+ do_check_eq(tempAddon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ tempAddon.uninstall();
+ unpacked_addon.remove(true);
+
+ addon.userDisabled = false;
+ addon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID);
+
+ // existing add-on is back
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ addon.uninstall();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseRestartManager();
+});
+
+// Installing a temporary add-on over a non-restartless add-on should fail.
+add_task(function*() {
+ yield promiseInstallAllFiles([do_get_addon("test_install1")], true);
+
+ let non_restartless_ID = "addon1@tests.mozilla.org";
+
+ BootstrapMonitor.checkAddonNotInstalled(non_restartless_ID);
+ BootstrapMonitor.checkAddonNotStarted(non_restartless_ID);
+
+ restartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(non_restartless_ID);
+ BootstrapMonitor.checkAddonNotStarted(non_restartless_ID);
+
+ let addon = yield promiseAddonByID(non_restartless_ID);
+
+ // non-restartless add-on is installed and started
+ do_check_neq(addon, null);
+ do_check_eq(addon.id, non_restartless_ID);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let tempdir = gTmpD.clone();
+ writeInstallRDFToDir({
+ id: non_restartless_ID,
+ version: "2.0",
+ bootstrap: true,
+ unpack: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test 1 (temporary)",
+ }, tempdir);
+
+ let unpacked_addon = tempdir.clone();
+ unpacked_addon.append(non_restartless_ID);
+
+ try {
+ yield AddonManager.installTemporaryAddon(unpacked_addon);
+ do_throw("Installing over a non-restartless add-on should return"
+ + " a rejected promise");
+ } catch (err) {
+ do_check_eq(err.message,
+ "Non-restartless add-on with ID addon1@tests.mozilla.org is"
+ + " already installed");
+ }
+
+ unpacked_addon.remove(true);
+ addon.uninstall();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+
+ yield promiseRestartManager();
+});
+
+// Installing a temporary add-on when there is already a temporary
+// add-on should fail.
+add_task(function*() {
+ yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
+
+ let addon = yield promiseAddonByID(ID);
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Test Bootstrap 1");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_eq(addon.type, "extension");
+ do_check_false(addon.isWebExtension);
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ yield promiseRestartManager();
+
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+});
+
+// Check that a temporary add-on is marked as such.
+add_task(function*() {
+ yield AddonManager.installTemporaryAddon(do_get_addon("test_bootstrap1_1"));
+ const addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null);
+ equal(addon.temporarilyInstalled, true);
+
+ yield promiseRestartManager();
+});
+
+// Check that a permanent add-on is not marked as temporarily installed.
+add_task(function*() {
+ yield promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
+ const addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null);
+ equal(addon.temporarilyInstalled, false);
+
+ yield promiseRestartManager();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_theme.js b/toolkit/mozapps/extensions/test/xpcshell/test_theme.js
new file mode 100644
index 000000000..84d6f1d0d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_theme.js
@@ -0,0 +1,1139 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/NetUtil.jsm");
+
+// The maximum allowable time since install. If an add-on claims to have been
+// installed longer ago than this the the test will fail.
+const MAX_INSTALL_TIME = 10000;
+
+// This verifies that themes behave as expected
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Observer to ensure a "lightweight-theme-styling-update" notification is sent
+// when expected
+var gLWThemeChanged = false;
+var LightweightThemeObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic != "lightweight-theme-styling-update")
+ return;
+
+ gLWThemeChanged = true;
+ }
+};
+
+AM_Cc["@mozilla.org/observer-service;1"]
+ .getService(Components.interfaces.nsIObserverService)
+ .addObserver(LightweightThemeObserver, "lightweight-theme-styling-update", false);
+
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, "theme1/1.0");
+ writeInstallRDFForExtension({
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ type: 4,
+ skinnable: true,
+ internalName: "theme1/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ skinnable: false,
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ // We need a default theme for some of these things to work but we have hidden
+ // the one in the application directory.
+ writeInstallRDFForExtension({
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Default",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ startupManager();
+ // Make sure we only register once despite multiple calls
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+ AddonManager.addAddonListener(AddonListener);
+ AddonManager.addInstallListener(InstallListener);
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"],
+ function([d, t1, t2]) {
+ do_check_neq(d, null);
+ do_check_false(d.skinnable);
+ do_check_false(d.foreignInstall);
+ do_check_eq(d.signedState, undefined);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_eq(t1.signedState, undefined);
+ do_check_true(t1.isActive);
+ do_check_true(t1.skinnable);
+ do_check_true(t1.foreignInstall);
+ do_check_eq(t1.screenshots, null);
+ do_check_true(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(t1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL |
+ AddonManager.OP_NEEDS_RESTART_DISABLE);
+
+ do_check_neq(t2, null);
+ do_check_true(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_eq(t2.signedState, undefined);
+ do_check_false(t2.isActive);
+ do_check_false(t2.skinnable);
+ do_check_true(t2.foreignInstall);
+ do_check_eq(t2.screenshots, null);
+ do_check_false(isThemeInAddonsList(profileDir, t2.id));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(t2.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+
+ do_execute_soon(run_test_1);
+ });
+}
+
+function end_test() {
+ do_execute_soon(do_test_finished);
+}
+
+// Checks enabling one theme disables the others
+function run_test_1() {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onDisabling"
+ ],
+ "theme2@tests.mozilla.org": [
+ "onEnabling"
+ ]
+ });
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_true(t1.userDisabled);
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_execute_soon(check_test_1);
+ });
+}
+
+function check_test_1() {
+ restartManager();
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme2/1.0");
+
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_false(t1.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(t1.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_ENABLE);
+
+ do_check_neq(t2, null);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_true(t2.isActive);
+ do_check_true(isThemeInAddonsList(profileDir, t2.id));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_false(hasFlag(t2.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_eq(t2.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_UNINSTALL |
+ AddonManager.OP_NEEDS_RESTART_DISABLE);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Removing the active theme should fall back to the default (not ideal in this
+// case since we don't have the default theme installed)
+function run_test_2() {
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("theme2@tests.mozilla.org"));
+ dest.remove(true);
+
+ restartManager();
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ AddonManager.getAddonsByIDs(["theme1@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([t1, t2]) {
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+ do_check_false(t1.appDisabled);
+ do_check_false(t1.isActive);
+ do_check_false(isThemeInAddonsList(profileDir, t1.id));
+ do_check_false(hasFlag(t1.permissions, AddonManager.PERM_CAN_DISABLE));
+ do_check_true(hasFlag(t1.permissions, AddonManager.PERM_CAN_ENABLE));
+
+ do_check_eq(t2, null);
+ do_check_false(isThemeInAddonsList(profileDir, "theme2@tests.mozilla.org"));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Installing a lightweight theme should happen instantly and disable the default theme
+function run_test_3() {
+ writeInstallRDFForExtension({
+ id: "theme2@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ internalName: "theme2/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+ restartManager();
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled",
+ ["onEnabling", false],
+ "onEnabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled",
+ ]
+ }, [
+ "onExternalInstall"
+ ]);
+
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost/data/index.html",
+ headerURL: "http://localhost/data/header.png",
+ footerURL: "http://localhost/data/footer.png",
+ previewURL: "http://localhost/data/preview.png",
+ iconURL: "http://localhost/data/icon.png"
+ };
+
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(null, p1);
+ do_check_eq(p1.name, "Test LW Theme");
+ do_check_eq(p1.version, "1");
+ do_check_eq(p1.type, "theme");
+ do_check_eq(p1.description, "A test theme");
+ do_check_eq(p1.creator, "Mozilla");
+ do_check_eq(p1.homepageURL, "http://localhost/data/index.html");
+ do_check_eq(p1.iconURL, "http://localhost/data/icon.png");
+ do_check_eq(p1.screenshots.length, 1);
+ do_check_eq(p1.screenshots[0], "http://localhost/data/preview.png");
+ do_check_false(p1.appDisabled);
+ do_check_false(p1.userDisabled);
+ do_check_true(p1.isCompatible);
+ do_check_true(p1.providesUpdatesSecurely);
+ do_check_eq(p1.blocklistState, 0);
+ do_check_true(p1.isActive);
+ do_check_eq(p1.pendingOperations, 0);
+ do_check_eq(p1.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE);
+ do_check_eq(p1.scope, AddonManager.SCOPE_PROFILE);
+ do_check_true("isCompatibleWith" in p1);
+ do_check_true("findUpdates" in p1);
+ do_check_eq(p1.installDate.getTime(), p1.updateDate.getTime());
+
+ // Should have been installed sometime in the last few seconds.
+ let difference = Date.now() - p1.installDate.getTime();
+ if (difference > MAX_INSTALL_TIME)
+ do_throw("Add-on was installed " + difference + "ms ago");
+ else if (difference < 0)
+ do_throw("Add-on was installed " + difference + "ms in the future");
+
+ AddonManager.getAddonsByTypes(["theme"], function(addons) {
+ let seen = false;
+ addons.forEach(function(a) {
+ if (a.id == "1@personas.mozilla.org") {
+ seen = true;
+ }
+ else {
+ dump("Checking theme " + a.id + "\n");
+ do_check_false(a.isActive);
+ do_check_true(a.userDisabled);
+ }
+ });
+ do_check_true(seen);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_4);
+ });
+ });
+}
+
+// Installing a second lightweight theme should disable the first with no restart
+function run_test_4() {
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled",
+ ],
+ "2@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled",
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ }, [
+ "onExternalInstall"
+ ]);
+
+ LightweightThemeManager.currentTheme = {
+ id: "2",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A second test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost/data/index.html",
+ headerURL: "http://localhost/data/header.png",
+ footerURL: "http://localhost/data/footer.png",
+ previewURL: "http://localhost/data/preview.png",
+ iconURL: "http://localhost/data/icon.png"
+ };
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsByIDs(["1@personas.mozilla.org",
+ "2@personas.mozilla.org"], function([p1, p2]) {
+ do_check_neq(null, p2);
+ do_check_false(p2.appDisabled);
+ do_check_false(p2.userDisabled);
+ do_check_true(p2.isActive);
+ do_check_eq(p2.pendingOperations, 0);
+ do_check_eq(p2.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_DISABLE);
+ do_check_eq(p2.installDate.getTime(), p2.updateDate.getTime());
+
+ // Should have been installed sometime in the last few seconds.
+ let difference = Date.now() - p2.installDate.getTime();
+ if (difference > MAX_INSTALL_TIME)
+ do_throw("Add-on was installed " + difference + "ms ago");
+ else if (difference < 0)
+ do_throw("Add-on was installed " + difference + "ms in the future");
+
+ do_check_neq(null, p1);
+ do_check_false(p1.appDisabled);
+ do_check_true(p1.userDisabled);
+ do_check_false(p1.isActive);
+ do_check_eq(p1.pendingOperations, 0);
+ do_check_eq(p1.permissions, AddonManager.PERM_CAN_UNINSTALL | AddonManager.PERM_CAN_ENABLE);
+
+ AddonManager.getAddonsByTypes(["theme"], function(addons) {
+ let seen = false;
+ addons.forEach(function(a) {
+ if (a.id == "2@personas.mozilla.org") {
+ seen = true;
+ }
+ else {
+ dump("Checking theme " + a.id + "\n");
+ do_check_false(a.isActive);
+ do_check_true(a.userDisabled);
+ }
+ });
+ do_check_true(seen);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_5);
+ });
+ });
+}
+
+// Switching to a custom theme should disable the lightweight theme and require
+// a restart. Cancelling that should also be possible.
+function run_test_5() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onDisabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onEnabling"
+ ]
+ });
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onOperationCancelled",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onDisabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onEnabling"
+ ]
+ });
+
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ do_check_false(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_true(hasFlag(AddonManager.PENDING_ENABLE, t2.pendingOperations));
+ do_check_true(p2.isActive);
+ do_check_true(p2.userDisabled);
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, p2.pendingOperations));
+ do_check_true(hasFlag(AddonManager.PERM_CAN_ENABLE, p2.permissions));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(check_test_5);
+ });
+}
+
+function check_test_5() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_ENABLE, t2.pendingOperations));
+ do_check_false(p2.isActive);
+ do_check_true(p2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, p2.pendingOperations));
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_6);
+ });
+}
+
+// Switching from a custom theme to a lightweight theme should require a restart
+function run_test_6() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onEnabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onOperationCancelled",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+
+ t2.userDisabled = false;
+
+ ensure_test_completed();
+
+ prepare_test({
+ "2@personas.mozilla.org": [
+ "onEnabling",
+ ],
+ "theme2@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ p2.userDisabled = false;
+
+ ensure_test_completed();
+
+ do_check_false(p2.isActive);
+ do_check_false(p2.userDisabled);
+ do_check_true(hasFlag(AddonManager.PENDING_ENABLE, p2.pendingOperations));
+ do_check_true(t2.isActive);
+ do_check_true(t2.userDisabled);
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, t2.pendingOperations));
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(check_test_6);
+ });
+}
+
+function check_test_6() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["2@personas.mozilla.org",
+ "theme2@tests.mozilla.org"], function([p2, t2]) {
+ do_check_true(p2.isActive);
+ do_check_false(p2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_ENABLE, p2.pendingOperations));
+ do_check_false(t2.isActive);
+ do_check_true(t2.userDisabled);
+ do_check_false(hasFlag(AddonManager.PENDING_DISABLE, t2.pendingOperations));
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_7);
+ });
+}
+
+// Uninstalling a lightweight theme should not require a restart
+function run_test_7() {
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ p1.uninstall();
+
+ ensure_test_completed();
+ do_check_eq(LightweightThemeManager.usedThemes.length, 1);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_8);
+ });
+}
+
+// Uninstalling a lightweight theme in use should not require a restart and it
+// should reactivate the default theme
+// Also, uninstalling a lightweight theme in use should send a
+// "lightweight-theme-styling-update" notification through the observer service
+function run_test_8() {
+ prepare_test({
+ "2@personas.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ AddonManager.getAddonByID("2@personas.mozilla.org", function(p2) {
+ p2.uninstall();
+
+ ensure_test_completed();
+ do_check_eq(LightweightThemeManager.usedThemes.length, 0);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_9);
+ });
+}
+
+// Uninstalling a theme not in use should not require a restart
+function run_test_9() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+
+ t1.uninstall();
+
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(newt1) {
+ do_check_eq(newt1, null);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_10);
+ });
+ });
+}
+
+// Uninstalling a custom theme in use should require a restart
+function run_test_10() {
+ AddonManager.getAddonByID("theme2@tests.mozilla.org", callback_soon(function(oldt2) {
+ prepare_test({
+ "theme2@tests.mozilla.org": [
+ "onEnabling",
+ ],
+ "default@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ oldt2.userDisabled = false;
+
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme2@tests.mozilla.org"], function([d, t2]) {
+ do_check_true(t2.isActive);
+ do_check_false(t2.userDisabled);
+ do_check_false(t2.appDisabled);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+
+ prepare_test({
+ "theme2@tests.mozilla.org": [
+ "onUninstalling",
+ ],
+ "default@tests.mozilla.org": [
+ "onEnabling"
+ ]
+ });
+
+ t2.uninstall();
+
+ ensure_test_completed();
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_11);
+ });
+ }));
+}
+
+// Installing a custom theme not in use should not require a restart
+function run_test_11() {
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(install.addon.skinnable, true);
+ do_check_false(hasFlag(install.addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_11);
+ install.install();
+ });
+}
+
+function check_test_11() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ do_check_neq(t1, null);
+ var previewSpec = do_get_addon_root_uri(profileDir, "theme1@tests.mozilla.org") + "preview.png";
+ do_check_eq(t1.screenshots.length, 1);
+ do_check_eq(t1.screenshots[0], previewSpec);
+ do_check_true(t1.skinnable);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_12);
+ });
+}
+
+// Updating a custom theme not in use should not require a restart
+function run_test_12() {
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_false(hasFlag(install.addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_12);
+ install.install();
+ });
+}
+
+function check_test_12() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", function(t1) {
+ do_check_neq(t1, null);
+ do_check_false(gLWThemeChanged);
+
+ do_execute_soon(run_test_13);
+ });
+}
+
+// Updating a custom theme in use should require a restart
+function run_test_13() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onEnabling",
+ ],
+ "default@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ t1.userDisabled = false;
+ ensure_test_completed();
+ restartManager();
+
+ prepare_test({ }, [
+ "onNewInstall"
+ ]);
+
+ AddonManager.getInstallForFile(do_get_addon("test_theme"), function(install) {
+ ensure_test_completed();
+
+ do_check_neq(install, null);
+ do_check_eq(install.type, "theme");
+ do_check_eq(install.version, "1.0");
+ do_check_eq(install.name, "Test Theme 1");
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ do_check_true(hasFlag(install.addon.operationsRequiringRestart, AddonManager.OP_NEEDS_RESTART_INSTALL));
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onInstalling",
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_13));
+ install.install();
+ });
+ }));
+}
+
+function check_test_13() {
+ restartManager();
+
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(gLWThemeChanged);
+ t1.uninstall();
+ restartManager();
+
+ do_execute_soon(run_test_14);
+ }));
+}
+
+// Switching from a lightweight theme to the default theme should not require
+// a restart
+function run_test_14() {
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost/data/index.html",
+ headerURL: "http://localhost/data/header.png",
+ footerURL: "http://localhost/data/footer.png",
+ previewURL: "http://localhost/data/preview.png",
+ iconURL: "http://localhost/data/icon.png"
+ };
+
+ AddonManager.getAddonByID("default@tests.mozilla.org", function(d) {
+ do_check_true(d.userDisabled);
+ do_check_false(d.isActive);
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+
+ d.userDisabled = false;
+ ensure_test_completed();
+
+ do_check_false(d.userDisabled);
+ do_check_true(d.isActive);
+
+ do_check_true(gLWThemeChanged);
+ gLWThemeChanged = false;
+
+ do_execute_soon(run_test_15);
+ });
+}
+
+// Upgrading the application with a custom theme in use should not disable it
+function run_test_15() {
+ restartManager();
+
+ installAllFiles([do_get_addon("test_theme")], function() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ t1.userDisabled = false;
+
+ restartManager();
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1/1.0");
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"],
+ callback_soon(function([d_2, t1_2]) {
+ do_check_true(d_2.userDisabled);
+ do_check_false(d_2.appDisabled);
+ do_check_false(d_2.isActive);
+
+ do_check_false(t1_2.userDisabled);
+ do_check_false(t1_2.appDisabled);
+ do_check_true(t1_2.isActive);
+
+ restartManager("2");
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1/1.0");
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"], function([d_3, t1_3]) {
+ do_check_true(d_3.userDisabled);
+ do_check_false(d_3.appDisabled);
+ do_check_false(d_3.isActive);
+
+ do_check_false(t1_3.userDisabled);
+ do_check_false(t1_3.appDisabled);
+ do_check_true(t1_3.isActive);
+
+ do_execute_soon(run_test_16);
+ });
+ }));
+ }));
+ });
+}
+
+// Upgrading the application with a custom theme in use should disable it if it
+// is no longer compatible
+function run_test_16() {
+ restartManager("3");
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"], function([d, t1]) {
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+
+ do_check_true(t1.userDisabled);
+ do_check_true(t1.appDisabled);
+ do_check_false(t1.isActive);
+
+ do_execute_soon(run_test_17);
+ });
+}
+
+// Verifies that if the selected theme pref is changed by a different version
+// of the application that we correctly reset it when it points to an
+// incompatible theme
+function run_test_17() {
+ restartManager("2");
+ shutdownManager();
+
+ Services.prefs.setCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN, "theme1/1.0");
+
+ restartManager("3");
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"], function([d, t1]) {
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+
+ do_check_true(t1.userDisabled);
+ do_check_true(t1.appDisabled);
+ do_check_false(t1.isActive);
+
+ do_execute_soon(run_test_18);
+ });
+}
+
+// Disabling the active theme should switch back to the default theme
+function run_test_18() {
+ restartManager(2);
+
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ t1.userDisabled = false;
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"],
+ callback_soon(function([d_2, t1_2]) {
+ do_check_true(d_2.userDisabled);
+ do_check_false(d_2.appDisabled);
+ do_check_false(d_2.isActive);
+
+ do_check_false(t1_2.userDisabled);
+ do_check_false(t1_2.appDisabled);
+ do_check_true(t1_2.isActive);
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onDisabling",
+ ],
+ "default@tests.mozilla.org": [
+ "onEnabling",
+ ]
+ });
+ t1_2.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_false(d_2.userDisabled);
+ do_check_false(d_2.appDisabled);
+ do_check_false(d_2.isActive);
+
+ do_check_true(t1_2.userDisabled);
+ do_check_false(t1_2.appDisabled);
+ do_check_true(t1_2.isActive);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "theme1@tests.mozilla.org"], function([d_3, t1_3]) {
+ do_check_false(d_3.userDisabled);
+ do_check_false(d_3.appDisabled);
+ do_check_true(d_3.isActive);
+
+ do_check_true(t1_3.userDisabled);
+ do_check_false(t1_3.appDisabled);
+ do_check_false(t1_3.isActive);
+
+ do_execute_soon(run_test_19);
+ });
+ }));
+ }));
+}
+
+// Disabling the active persona should switch back to the default theme
+function run_test_19() {
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "1@personas.mozilla.org"], function([d, p1]) {
+ p1.userDisabled = false;
+
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_false(d.isActive);
+
+ do_check_false(p1.userDisabled);
+ do_check_false(p1.appDisabled);
+ do_check_true(p1.isActive);
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ],
+ "default@tests.mozilla.org": [
+ ["onEnabling", false],
+ "onEnabled"
+ ]
+ });
+ p1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+
+ do_check_true(p1.userDisabled);
+ do_check_false(p1.appDisabled);
+ do_check_false(p1.isActive);
+
+ do_execute_soon(run_test_20);
+ });
+}
+
+// Tests that you cannot disable the default theme
+function run_test_20() {
+ AddonManager.getAddonByID("default@tests.mozilla.org", function(d) {
+ do_check_false(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_true(d.isActive);
+
+ try {
+ d.userDisabled = true;
+ do_throw("Disabling the default theme should throw an exception");
+ }
+ catch (e) {
+ }
+
+ do_execute_soon(run_test_21);
+ });
+}
+
+// Tests that cached copies of a lightweight theme have the right permissions
+// and pendingOperations during the onEnabling event
+function run_test_21() {
+ AddonManager.getAddonByID("theme1@tests.mozilla.org", callback_soon(function(t1) {
+ // Switch to a custom theme so we can test pendingOperations properly.
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onEnabling"
+ ],
+ "default@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ t1.userDisabled = false;
+ ensure_test_completed();
+
+ restartManager();
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ AddonManager.addAddonListener({
+ onEnabling: function(aAddon) {
+ do_check_false(hasFlag(aAddon.permissions, AddonManager.PERM_CAN_ENABLE));
+ do_check_true(hasFlag(aAddon.pendingOperations, AddonManager.PENDING_ENABLE));
+
+ do_check_eq(aAddon.permissions, p1.permissions);
+ do_check_eq(aAddon.pendingOperations, p1.pendingOperations);
+ }
+ });
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ "onEnabling"
+ ],
+ "theme1@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+
+ p1.userDisabled = false;
+ ensure_test_completed();
+
+ run_test_22();
+ });
+ }));
+}
+
+// Detecting a new add-on during the startup file check should not disable an
+// active lightweight theme
+function run_test_22() {
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "1@personas.mozilla.org"], function([d, p1]) {
+ do_check_true(d.userDisabled);
+ do_check_false(d.appDisabled);
+ do_check_false(d.isActive);
+
+ do_check_false(p1.userDisabled);
+ do_check_false(p1.appDisabled);
+ do_check_true(p1.isActive);
+
+ writeInstallRDFForExtension({
+ id: "theme3@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 3",
+ internalName: "theme3/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }]
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["default@tests.mozilla.org",
+ "1@personas.mozilla.org"], function([d_2, p1_2]) {
+ do_check_true(d_2.userDisabled);
+ do_check_false(d_2.appDisabled);
+ do_check_false(d_2.isActive);
+
+ do_check_false(p1_2.userDisabled);
+ do_check_false(p1_2.appDisabled);
+ do_check_true(p1_2.isActive);
+
+ end_test();
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_types.js b/toolkit/mozapps/extensions/test/xpcshell/test_types.js
new file mode 100644
index 000000000..679f4808c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_types.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that custom types can be defined and undefined
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+function run_test() {
+ startupManager();
+
+ do_check_false("test" in AddonManager.addonTypes);
+ let types = AddonManager.addonTypes;
+
+ // The dumbest provider possible
+ var provider = {
+ };
+
+ var expectedAdd = "test";
+ var expectedRemove = null;
+
+ AddonManager.addTypeListener({
+ onTypeAdded: function(aType) {
+ do_check_eq(aType.id, expectedAdd);
+ expectedAdd = null;
+ },
+
+ onTypeRemoved: function(aType) {
+ do_check_eq(aType.id, expectedRemove);
+ expectedRemove = null;
+ }
+ });
+
+ AddonManagerPrivate.registerProvider(provider, [{
+ id: "test",
+ name: "Test",
+ uiPriority: 1
+ }, {
+ id: "t$e%st",
+ name: "Test",
+ uiPriority: 1
+ }]);
+
+ do_check_eq(expectedAdd, null);
+
+ do_check_true("test" in types);
+ do_check_eq(types["test"].name, "Test");
+ do_check_false("t$e%st" in types);
+
+ delete types["test"];
+ do_check_true("test" in types);
+
+ types["foo"] = "bar";
+ do_check_false("foo" in types);
+
+ expectedRemove = "test";
+
+ AddonManagerPrivate.unregisterProvider(provider);
+
+ do_check_eq(expectedRemove, null);
+
+ do_check_false("test" in AddonManager.addonTypes);
+ // The cached reference to addonTypes is live
+ do_check_false("test" in types);
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_undothemeuninstall.js b/toolkit/mozapps/extensions/test/xpcshell/test_undothemeuninstall.js
new file mode 100644
index 000000000..36ca95aec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_undothemeuninstall.js
@@ -0,0 +1,423 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that forcing undo for uninstall works for themes
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const PREF_GENERAL_SKINS_SELECTEDSKIN = "general.skins.selectedSkin";
+
+var defaultTheme = {
+ id: "default@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ internalName: "classic/1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+var theme1 = {
+ id: "theme1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ internalName: "theme1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function dummyLWTheme(id) {
+ return {
+ id: id || Math.random().toString(),
+ name: Math.random().toString(),
+ headerURL: "http://lwttest.invalid/a.png",
+ footerURL: "http://lwttest.invalid/b.png",
+ textcolor: Math.random().toString(),
+ accentcolor: Math.random().toString()
+ };
+}
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ do_register_cleanup(promiseShutdownManager);
+
+ run_next_test();
+}
+
+add_task(function* checkDefault() {
+ writeInstallRDFForExtension(defaultTheme, profileDir);
+ yield promiseRestartManager();
+
+ let d = yield promiseAddonByID("default@tests.mozilla.org");
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+// Tests that uninstalling an enabled theme offers the option to undo
+add_task(function* uninstallEnabledOffersUndo() {
+ writeInstallRDFForExtension(theme1, profileDir);
+
+ yield promiseRestartManager();
+
+ let t1 = yield promiseAddonByID("theme1@tests.mozilla.org");
+
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+
+ t1.userDisabled = false;
+
+ yield promiseRestartManager();
+
+ let d = null;
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+ prepare_test({
+ "default@tests.mozilla.org": [
+ "onEnabling"
+ ],
+ "theme1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ t1.uninstall(true);
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+ yield promiseRestartManager();
+
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(t1, null);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+// Tests that uninstalling an enabled theme can be undone
+add_task(function* canUndoUninstallEnabled() {
+ writeInstallRDFForExtension(theme1, profileDir);
+
+ yield promiseRestartManager();
+
+ let t1 = yield promiseAddonByID("theme1@tests.mozilla.org");
+
+ do_check_neq(t1, null);
+ do_check_true(t1.userDisabled);
+
+ t1.userDisabled = false;
+
+ yield promiseRestartManager();
+
+ let d = null;
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+ prepare_test({
+ "default@tests.mozilla.org": [
+ "onEnabling"
+ ],
+ "theme1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ t1.uninstall(true);
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+ prepare_test({
+ "default@tests.mozilla.org": [
+ "onOperationCancelled"
+ ],
+ "theme1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ t1.cancelUninstall();
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ yield promiseRestartManager();
+
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "theme1");
+
+ t1.uninstall();
+ yield promiseRestartManager();
+});
+
+// Tests that uninstalling a disabled theme offers the option to undo
+add_task(function* uninstallDisabledOffersUndo() {
+ writeInstallRDFForExtension(theme1, profileDir);
+
+ yield promiseRestartManager();
+
+ let [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ t1.uninstall(true);
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ yield promiseRestartManager();
+
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(t1, null);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+});
+
+// Tests that uninstalling a disabled theme can be undone
+add_task(function* canUndoUninstallDisabled() {
+ writeInstallRDFForExtension(theme1, profileDir);
+
+ yield promiseRestartManager();
+
+ let [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ t1.uninstall(true);
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ prepare_test({
+ "theme1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ t1.cancelUninstall();
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ yield promiseRestartManager();
+
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@tests.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_false(t1.isActive);
+ do_check_true(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ t1.uninstall();
+ yield promiseRestartManager();
+});
+
+// Tests that uninstalling an enabled lightweight theme offers the option to undo
+add_task(function* uninstallLWTOffersUndo() {
+ // skipped since lightweight themes don't support undoable uninstall yet
+ return;
+ /*
+ LightweightThemeManager.currentTheme = dummyLWTheme("theme1");
+
+ let [ t1, d ] = yield promiseAddonsByIDs(["theme1@personas.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_true(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_neq(t1, null);
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_eq(t1.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ prepare_test({
+ "default@tests.mozilla.org": [
+ "onEnabling"
+ ],
+ "theme1@personas.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ t1.uninstall(true);
+ ensure_test_completed();
+
+ do_check_neq(d, null);
+ do_check_false(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_ENABLE);
+
+ do_check_true(t1.isActive);
+ do_check_false(t1.userDisabled);
+ do_check_true(hasFlag(t1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+
+ yield promiseRestartManager();
+
+ [ t1, d ] = yield promiseAddonsByIDs(["theme1@personas.mozilla.org",
+ "default@tests.mozilla.org"]);
+
+ do_check_neq(d, null);
+ do_check_true(d.isActive);
+ do_check_false(d.userDisabled);
+ do_check_eq(d.pendingOperations, AddonManager.PENDING_NONE);
+
+ do_check_eq(t1, null);
+
+ do_check_eq(Services.prefs.getCharPref(PREF_GENERAL_SKINS_SELECTEDSKIN), "classic/1.0");
+ */
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
new file mode 100644
index 000000000..4680a3c4a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_undouninstall.js
@@ -0,0 +1,792 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that forcing undo for uninstall works
+
+const APP_STARTUP = 1;
+const APP_SHUTDOWN = 2;
+const ADDON_ENABLE = 3;
+const ADDON_DISABLE = 4;
+const ADDON_INSTALL = 5;
+const ADDON_UNINSTALL = 6;
+const ADDON_UPGRADE = 7;
+const ADDON_DOWNGRADE = 8;
+
+const ID = "undouninstall1@tests.mozilla.org";
+const INCOMPAT_ID = "incompatible@tests.mozilla.org";
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+BootstrapMonitor.init();
+
+function getStartupReason(id) {
+ let info = BootstrapMonitor.started.get(id);
+ return info ? info.reason : undefined;
+}
+
+function getShutdownReason(id) {
+ let info = BootstrapMonitor.stopped.get(id);
+ return info ? info.reason : undefined;
+}
+
+function getInstallReason(id) {
+ let info = BootstrapMonitor.installed.get(id);
+ return info ? info.reason : undefined;
+}
+
+function getUninstallReason(id) {
+ let info = BootstrapMonitor.uninstalled.get(id);
+ return info ? info.reason : undefined;
+}
+
+function getStartupOldVersion(id) {
+ let info = BootstrapMonitor.started.get(id);
+ return info ? info.data.oldVersion : undefined;
+}
+
+function getShutdownNewVersion(id) {
+ let info = BootstrapMonitor.stopped.get(id);
+ return info ? info.data.newVersion : undefined;
+}
+
+function getInstallOldVersion(id) {
+ let info = BootstrapMonitor.installed.get(id);
+ return info ? info.data.oldVersion : undefined;
+}
+
+function getUninstallNewVersion(id) {
+ let info = BootstrapMonitor.uninstalled.get(id);
+ return info ? info.data.newVersion : undefined;
+}
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ startupManager();
+ do_register_cleanup(promiseShutdownManager);
+
+ run_next_test();
+}
+
+add_task(function* installAddon() {
+ let olda1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_eq(olda1, null);
+
+ writeInstallRDFForExtension(addon1, profileDir);
+ yield promiseRestartManager();
+
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.pendingOperations, 0);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+});
+
+// Uninstalling an add-on should work.
+add_task(function* uninstallAddon() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_eq(a1.pendingOperations, 0);
+ do_check_neq(a1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+ a1.uninstall(true);
+ do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ ensure_test_completed();
+
+ let list = yield promiseAddonsWithOperationsByTypes(null);
+
+ do_check_eq(list.length, 1);
+ do_check_eq(list[0].id, "addon1@tests.mozilla.org");
+
+ yield promiseRestartManager();
+
+ a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_eq(a1, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ do_check_false(dest.exists());
+ writeInstallRDFForExtension(addon1, profileDir);
+ yield promiseRestartManager();
+});
+
+// Cancelling the uninstall should send onOperationCancelled
+add_task(function* cancelUninstall() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.pendingOperations, 0);
+ a1.uninstall(true);
+ do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ do_check_eq(a1.pendingOperations, 0);
+
+ ensure_test_completed();
+ yield promiseRestartManager();
+
+ a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+});
+
+// Uninstalling an item pending disable should still require a restart
+add_task(function* pendingDisableRequestRestart() {
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+ a1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+ do_check_true(a1.isActive);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+
+ yield promiseRestartManager();
+});
+
+// Test that uninstalling an inactive item should still allow cancelling
+add_task(function* uninstallInactiveIsCancellable() {
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+
+ yield promiseRestartManager();
+});
+
+// Test that an inactive item can be uninstalled
+add_task(function* uninstallInactive() {
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ [ "onUninstalling", false ],
+ "onUninstalled"
+ ]
+ });
+ a1.uninstall();
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+ do_check_eq(a1, null);
+});
+
+// Tests that an enabled restartless add-on can be uninstalled and goes away
+// when the uninstall is committed
+add_task(function* uninstallRestartless() {
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+ ensure_test_completed();
+
+ let a1 = yield promiseAddonByID(ID);
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID(ID);
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ // complete the uinstall
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalled"
+ ]
+ });
+ a1.uninstall();
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID(ID);
+
+ do_check_eq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+});
+
+// Tests that an enabled restartless add-on can be uninstalled and then cancelled
+add_task(function* cancelUninstallOfRestartless() {
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+ ensure_test_completed();
+
+ let a1 = yield promiseAddonByID(ID);
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ shutdownManager();
+
+ do_check_eq(getShutdownReason(ID), APP_SHUTDOWN);
+ do_check_eq(getShutdownNewVersion(ID), undefined);
+
+ startupManager(false);
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getStartupReason(ID), APP_STARTUP);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ a1.uninstall();
+});
+
+// Tests that reinstalling an enabled restartless add-on waiting to be
+// uninstalled aborts the uninstall and leaves the add-on enabled
+add_task(function* reinstallAddonAwaitingUninstall() {
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+ let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_UNINSTALL);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getUninstallReason(ID), ADDON_DOWNGRADE);
+ do_check_eq(getInstallReason(ID), ADDON_DOWNGRADE);
+ do_check_eq(getStartupReason(ID), ADDON_DOWNGRADE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ shutdownManager();
+
+ do_check_eq(getShutdownReason(ID), APP_SHUTDOWN);
+
+ startupManager(false);
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getStartupReason(ID), APP_STARTUP);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ a1.uninstall();
+});
+
+// Tests that a disabled restartless add-on can be uninstalled and goes away
+// when the uninstall is committed
+add_task(function* uninstallDisabledRestartless() {
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+ let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ a1.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ // commit the uninstall
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalled"
+ ]
+ });
+ a1.uninstall();
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_eq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonNotInstalled(ID);
+ do_check_eq(getUninstallReason(ID), ADDON_UNINSTALL);
+});
+
+// Tests that a disabled restartless add-on can be uninstalled and then cancelled
+add_task(function* cancelUninstallDisabledRestartless() {
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+ ensure_test_completed();
+
+ let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onDisabling", false],
+ "onDisabled"
+ ]
+ });
+ a1.userDisabled = true;
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ yield promiseRestartManager();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ BootstrapMonitor.checkAddonInstalled(ID);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ a1.uninstall();
+});
+
+// Tests that reinstalling a disabled restartless add-on waiting to be
+// uninstalled aborts the uninstall and leaves the add-on disabled
+add_task(function* reinstallDisabledAddonAwaitingUninstall() {
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+ let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_INSTALL);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ a1.userDisabled = true;
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_eq(getShutdownReason(ID), ADDON_DISABLE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onNewInstall",
+ "onInstallStarted",
+ "onInstallEnded"
+ ]);
+
+ yield promiseInstallAllFiles([do_get_addon("test_undouninstall1")]);
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+ do_check_eq(getUninstallReason(ID), ADDON_DOWNGRADE);
+ do_check_eq(getInstallReason(ID), ADDON_DOWNGRADE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ yield promiseRestartManager();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+
+ a1.uninstall();
+});
+
+
+// Test that uninstalling a temporary addon can be canceled
+add_task(function* cancelUninstallTemporary() {
+ yield AddonManager.installTemporaryAddon(do_get_addon("test_undouninstall1"));
+
+ let a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(getInstallReason(ID), ADDON_INSTALL);
+ do_check_eq(getStartupReason(ID), ADDON_ENABLE);
+ do_check_eq(a1.pendingOperations, AddonManager.PENDING_NONE);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+ prepare_test({
+ "undouninstall1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID("undouninstall1@tests.mozilla.org");
+
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+ do_check_eq(a1.pendingOperations, 0);
+
+ yield promiseRestartManager();
+});
+
+// Tests that cancelling the uninstall of an incompatible restartless addon
+// does not start the addon
+add_task(function* cancelUninstallIncompatibleRestartless() {
+ yield promiseInstallAllFiles([do_get_addon("test_undoincompatible")]);
+
+ let a1 = yield promiseAddonByID(INCOMPAT_ID);
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(INCOMPAT_ID);
+ do_check_false(a1.isActive);
+
+ prepare_test({
+ "incompatible@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall(true);
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID(INCOMPAT_ID);
+ do_check_neq(a1, null);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+ do_check_false(a1.isActive);
+
+ prepare_test({
+ "incompatible@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+
+ a1 = yield promiseAddonByID(INCOMPAT_ID);
+ do_check_neq(a1, null);
+ BootstrapMonitor.checkAddonNotStarted(INCOMPAT_ID);
+ do_check_eq(a1.pendingOperations, 0);
+ do_check_false(a1.isActive);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js b/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
new file mode 100644
index 000000000..6b12489f2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_uninstall.js
@@ -0,0 +1,216 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-ons can be uninstalled.
+
+var addon1 = {
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ name: "Test 1",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }]
+};
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+// Sets up the profile by installing an add-on.
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ do_test_pending();
+ startupManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+ do_check_eq(olda1, null);
+
+ writeInstallRDFForExtension(addon1, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.pendingOperations, 0);
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ do_execute_soon(run_test_1);
+ });
+ }));
+}
+
+function end_test() {
+ do_execute_soon(do_test_finished);
+}
+
+// Uninstalling an add-on should work.
+function run_test_1() {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_eq(a1.pendingOperations, 0);
+ do_check_neq(a1.operationsRequiringRestart &
+ AddonManager.OP_NEEDS_RESTART_UNINSTALL, 0);
+ a1.uninstall();
+ do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+ do_check_in_crash_annotation(addon1.id, addon1.version);
+
+ ensure_test_completed();
+
+ AddonManager.getAddonsWithOperationsByTypes(null, function(list) {
+ do_check_eq(list.length, 1);
+ do_check_eq(list[0].id, "addon1@tests.mozilla.org");
+
+ do_execute_soon(check_test_1);
+ });
+ });
+}
+
+function check_test_1() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_eq(a1, null);
+ do_check_false(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+ do_check_not_in_crash_annotation(addon1.id, addon1.version);
+
+ var dest = profileDir.clone();
+ dest.append(do_get_expected_addon_name("addon1@tests.mozilla.org"));
+ do_check_false(dest.exists());
+ writeInstallRDFForExtension(addon1, profileDir);
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Cancelling the uninstall should send onOperationCancelled
+function run_test_2() {
+ restartManager();
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.pendingOperations, 0);
+ a1.uninstall();
+ do_check_true(hasFlag(a1.pendingOperations, AddonManager.PENDING_UNINSTALL));
+
+ ensure_test_completed();
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ do_check_eq(a1.pendingOperations, 0);
+
+ ensure_test_completed();
+
+ do_execute_soon(check_test_2);
+ });
+}
+
+function check_test_2() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(a1.isActive);
+ do_check_false(a1.userDisabled);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ run_test_3();
+ });
+}
+
+// Uninstalling an item pending disable should still require a restart
+function run_test_3() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onDisabling"
+ ]
+ });
+ a1.userDisabled = true;
+ ensure_test_completed();
+
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+ do_check_true(a1.isActive);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onUninstalling"
+ ]
+ });
+ a1.uninstall();
+
+ check_test_3();
+ });
+}
+
+function check_test_3() {
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_true(hasFlag(AddonManager.PENDING_UNINSTALL, a1.pendingOperations));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onOperationCancelled"
+ ]
+ });
+ a1.cancelUninstall();
+ ensure_test_completed();
+ do_check_true(hasFlag(AddonManager.PENDING_DISABLE, a1.pendingOperations));
+
+ do_execute_soon(run_test_4);
+ });
+}
+
+// Test that uninstalling an inactive item should happen without a restart
+function run_test_4() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_false(a1.isActive);
+ do_check_true(a1.userDisabled);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onUninstalling", false],
+ "onUninstalled"
+ ]
+ });
+ a1.uninstall();
+ ensure_test_completed();
+
+ check_test_4();
+ });
+}
+
+function check_test_4() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_eq(a1, null);
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update.js b/toolkit/mozapps/extensions/test/xpcshell/test_update.js
new file mode 100644
index 000000000..4db488ab5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update.js
@@ -0,0 +1,1398 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-on update checks work
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+// This test requires lightweight themes update to be enabled even if the app
+// doesn't support lightweight themes.
+Services.prefs.setBoolPref("lightweightThemes.update.enabled", true);
+
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const PARAMS = "?%REQ_VERSION%/%ITEM_ID%/%ITEM_VERSION%/%ITEM_MAXAPPVERSION%/" +
+ "%ITEM_STATUS%/%APP_ID%/%APP_VERSION%/%CURRENT_APP_VERSION%/" +
+ "%APP_OS%/%APP_ABI%/%APP_LOCALE%/%UPDATE_TYPE%";
+
+var gInstallDate;
+
+var testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_update.rdf", testserver);
+mapFile("/data/test_update.json", testserver);
+mapFile("/data/test_update.xml", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+var originalSyncGUID;
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+
+ run_next_test();
+}
+
+let testParams = [
+ { updateFile: "test_update.rdf",
+ appId: "xpcshell@tests.mozilla.org" },
+ { updateFile: "test_update.json",
+ appId: "toolkit@mozilla.org" },
+];
+
+for (let test of testParams) {
+ let { updateFile, appId } = test;
+
+ add_test(function() {
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }],
+ name: "Test Addon 2",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "5",
+ maxVersion: "5"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ startupManager();
+
+ run_next_test();
+ });
+
+ // Verify that an update is available and can be installed.
+ let check_test_1;
+ add_test(function run_test_1() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_eq(a1.releaseNotesURI, null);
+ do_check_true(a1.foreignInstall);
+ do_check_neq(a1.syncGUID, null);
+
+ originalSyncGUID = a1.syncGUID;
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onPropertyChanged", ["applyBackgroundUpdates"]]
+ ]
+ });
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ check_test_completed();
+
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ prepare_test({}, [
+ "onNewInstall",
+ ]);
+
+ a1.findUpdates({
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ensure_test_completed();
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], install);
+
+ do_check_eq(addon, a1);
+ do_check_eq(install.name, addon.name);
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.existingAddon, addon);
+ do_check_eq(install.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+
+ // Verify that another update check returns the same AddonInstall
+ a1.findUpdates({
+ onNoCompatibilityUpdateAvailable: function() {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function(newAddon, newInstall) {
+ AddonManager.getAllInstalls(function(aInstalls2) {
+ do_check_eq(aInstalls2.length, 1);
+ do_check_eq(aInstalls2[0], install);
+ do_check_eq(newAddon, addon);
+ do_check_eq(newInstall, install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_1);
+ install.install();
+ });
+ },
+
+ onNoUpdateAvailable: function() {
+ ok(false, "Should not have seen onNoUpdateAvailable notification");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoUpdateAvailable notification");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ let run_test_2;
+ check_test_1 = (install) => {
+ ensure_test_completed();
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ run_test_2(install);
+ return false;
+ };
+
+ // Continue installing the update.
+ let check_test_2;
+ run_test_2 = (install) => {
+ // Verify that another update check returns no new update
+ install.existingAddon.findUpdates({
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should find no available update when one is already downloading");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], install);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_2);
+ install.install();
+ });
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ };
+
+ check_test_2 = () => {
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+ do_check_neq(olda1, null);
+ do_check_eq(olda1.version, "1.0");
+ do_check_true(isExtensionInAddonsList(profileDir, olda1.id));
+
+ shutdownManager();
+
+ startupManager();
+
+ do_check_true(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
+ do_check_eq(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+ do_check_true(a1.foreignInstall);
+ do_check_neq(a1.syncGUID, null);
+ do_check_eq(originalSyncGUID, a1.syncGUID);
+
+ // Make sure that the extension lastModifiedTime was updated.
+ let testURI = a1.getResourceURI(TEST_UNPACKED ? "install.rdf" : "");
+ let testFile = testURI.QueryInterface(Components.interfaces.nsIFileURL).file;
+ let difference = testFile.lastModifiedTime - Date.now();
+ do_check_true(Math.abs(difference) < MAX_TIME_DIFFERENCE);
+
+ a1.uninstall();
+ run_next_test();
+ });
+ }));
+ };
+
+ // Check that an update check finds compatibility updates and applies them
+ let check_test_3;
+ add_test(function run_test_3() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isCompatibleWith("0", "0"));
+
+ a2.findUpdates({
+ onCompatibilityUpdateAvailable: function(addon) {
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ do_check_true(a2.isActive);
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_eq(addon, a2);
+ do_execute_soon(check_test_3);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ check_test_3 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ a2.uninstall();
+
+ run_next_test();
+ });
+ }
+
+ // Checks that we see no compatibility information when there is none.
+ add_test(function run_test_4() {
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_true(a3.isCompatibleWith("5", "5"));
+ do_check_false(a3.isCompatibleWith("2", "2"));
+
+ a3.findUpdates({
+ sawUpdate: false,
+ onCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen compatibility information");
+ },
+
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ this.sawUpdate = true;
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_true(this.sawUpdate);
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ // Checks that compatibility info for future apps are detected but don't make
+ // the item compatibile.
+ let check_test_5;
+ add_test(function run_test_5() {
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_true(a3.isCompatibleWith("5", "5"));
+ do_check_false(a3.isCompatibleWith("2", "2"));
+
+ a3.findUpdates({
+ sawUpdate: false,
+ onCompatibilityUpdateAvailable: function(addon) {
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+ this.sawUpdate = true;
+ },
+
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should have seen some compatibility information");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_true(this.sawUpdate);
+ do_execute_soon(check_test_5);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED, "3.0", "3.0");
+ });
+ });
+
+ check_test_5 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+
+ a3.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks work
+ let continue_test_6;
+ add_test(function run_test_6() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({}, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], continue_test_6);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+
+ let check_test_6;
+ continue_test_6 = (install) => {
+ do_check_neq(install.existingAddon, null);
+ do_check_eq(install.existingAddon.id, "addon1@tests.mozilla.org");
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_6));
+ }
+
+ check_test_6 = (install) => {
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ restartManager();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_eq(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+ a1.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Verify the parameter escaping in update urls.
+ add_test(function run_test_8() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "5.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "67.0.5b1",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "0",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 2",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.3+",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }, {
+ id: "toolkit@mozilla.org",
+ minVersion: "0",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "0.5ab6",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "5"
+ }],
+ name: "Test Addon 4",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 5",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 6",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
+ a2.userDisabled = true;
+ restartManager();
+
+ testserver.registerPathHandler("/data/param_test.rdf", function(request, response) {
+ do_check_neq(request.queryString, "");
+ let [req_version, item_id, item_version,
+ item_maxappversion, item_status,
+ app_id, app_version, current_app_version,
+ app_os, app_abi, app_locale, update_type] =
+ request.queryString.split("/").map(a => decodeURIComponent(a));
+
+ do_check_eq(req_version, "2");
+
+ switch (item_id) {
+ case "addon1@tests.mozilla.org":
+ do_check_eq(item_version, "5.0");
+ do_check_eq(item_maxappversion, "2");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "97");
+ break;
+ case "addon2@tests.mozilla.org":
+ do_check_eq(item_version, "67.0.5b1");
+ do_check_eq(item_maxappversion, "3");
+ do_check_eq(item_status, "userDisabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "49");
+ break;
+ case "addon3@tests.mozilla.org":
+ do_check_eq(item_version, "1.3+");
+ do_check_eq(item_maxappversion, "0");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "112");
+ break;
+ case "addon4@tests.mozilla.org":
+ do_check_eq(item_version, "0.5ab6");
+ do_check_eq(item_maxappversion, "5");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "2");
+ do_check_eq(update_type, "98");
+ break;
+ case "addon5@tests.mozilla.org":
+ do_check_eq(item_version, "1.0");
+ do_check_eq(item_maxappversion, "1");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "35");
+ break;
+ case "addon6@tests.mozilla.org":
+ do_check_eq(item_version, "1.0");
+ do_check_eq(item_maxappversion, "1");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "99");
+ break;
+ default:
+ ok(false, "Update request for unexpected add-on " + item_id);
+ }
+
+ do_check_eq(app_id, "xpcshell@tests.mozilla.org");
+ do_check_eq(current_app_version, "1");
+ do_check_eq(app_os, "XPCShell");
+ do_check_eq(app_abi, "noarch-spidermonkey");
+ do_check_eq(app_locale, "fr-FR");
+
+ request.setStatusLine(null, 500, "Server Error");
+ });
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org"],
+ function([a1_2, a2_2, a3_2, a4_2, a5_2, a6_2]) {
+ let count = 6;
+
+ function next_test() {
+ a1_2.uninstall();
+ a2_2.uninstall();
+ a3_2.uninstall();
+ a4_2.uninstall();
+ a5_2.uninstall();
+ a6_2.uninstall();
+
+ restartManager();
+ run_next_test();
+ }
+
+ let compatListener = {
+ onUpdateFinished: function(addon, error) {
+ if (--count == 0)
+ do_execute_soon(next_test);
+ }
+ };
+
+ let updateListener = {
+ onUpdateAvailable: function(addon, update) {
+ // Dummy so the update checker knows we care about new versions
+ },
+
+ onUpdateFinished: function(addon, error) {
+ if (--count == 0)
+ do_execute_soon(next_test);
+ }
+ };
+
+ a1_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ a2_2.findUpdates(compatListener, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ a3_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ a4_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, "2");
+ a5_2.findUpdates(compatListener, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ a6_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ });
+ }));
+ });
+
+ // Tests that if an install.rdf claims compatibility then the add-on will be
+ // seen as compatible regardless of what the update.rdf says.
+ add_test(function run_test_9() {
+ writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "5.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ do_check_true(a4.isActive, "addon4 is active");
+ do_check_true(a4.isCompatible, "addon4 is compatible");
+
+ run_next_test();
+ });
+ });
+
+ // Tests that a normal update check won't decrease a targetApplication's
+ // maxVersion.
+ add_test(function run_test_10() {
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ a4.findUpdates({
+ onUpdateFinished: function(addon) {
+ do_check_true(addon.isCompatible, "addon4 is compatible");
+
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ });
+ });
+
+ // Tests that an update check for a new application will decrease a
+ // targetApplication's maxVersion.
+ add_test(function run_test_11() {
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ a4.findUpdates({
+ onUpdateFinished: function(addon) {
+ do_check_true(addon.isCompatible, "addon4 is not compatible");
+
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ });
+ });
+
+ // Check that the decreased maxVersion applied and disables the add-on
+ add_test(function run_test_12() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ do_check_true(a4.isActive);
+ do_check_true(a4.isCompatible);
+
+ a4.uninstall();
+ run_next_test();
+ });
+ });
+
+ // Tests that a compatibility update is passed to the listener when there is
+ // compatibility info for the current version of the app but not for the
+ // version of the app that the caller requested an update check for, when
+ // strict compatibility checking is disabled.
+ let check_test_13;
+ add_test(function run_test_13() {
+ restartManager();
+
+ // Not initially compatible but the update check will make it compatible
+ writeInstallRDFForExtension({
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }],
+ name: "Test Addon 7",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon7@tests.mozilla.org", function(a7) {
+ do_check_neq(a7, null);
+ do_check_true(a7.isActive);
+ do_check_true(a7.isCompatible);
+ do_check_false(a7.appDisabled);
+ do_check_true(a7.isCompatibleWith("0", "0"));
+
+ a7.findUpdates({
+ sawUpdate: false,
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should have seen compatibility information");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function(addon) {
+ do_check_true(addon.isCompatible);
+ do_execute_soon(check_test_13);
+ }
+ }, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, "3.0", "3.0");
+ });
+ });
+
+ check_test_13 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon7@tests.mozilla.org", function(a7) {
+ do_check_neq(a7, null);
+ do_check_true(a7.isActive);
+ do_check_true(a7.isCompatible);
+ do_check_false(a7.appDisabled);
+
+ a7.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks doesn't update an add-on that isn't
+ // allowed to update automatically.
+ let check_test_14;
+ add_test(function run_test_14() {
+ restartManager();
+
+ // Have an add-on there that will be updated so we see some events from it
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 8",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon8@tests.mozilla.org", function(a8) {
+ a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ // The background update check will find updates for both add-ons but only
+ // proceed to install one of them.
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ let id = aInstall.existingAddon.id;
+ ok((id == "addon1@tests.mozilla.org" || id == "addon8@tests.mozilla.org"),
+ "Saw unexpected onNewInstall for " + id);
+ },
+
+ onDownloadStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadFailed: function(aInstall) {
+ ok(false, "Should not have seen onDownloadFailed event");
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ ok(false, "Should not have seen onDownloadCancelled event");
+ },
+
+ onInstallStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onInstallEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ do_check_eq(aInstall.existingAddon.pendingUpgrade.install, aInstall);
+
+ do_execute_soon(check_test_14);
+ },
+
+ onInstallFailed: function(aInstall) {
+ ok(false, "Should not have seen onInstallFailed event");
+ },
+
+ onInstallCancelled: function(aInstall) {
+ ok(false, "Should not have seen onInstallCancelled event");
+ },
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+ });
+
+ check_test_14 = () => {
+ restartManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon8@tests.mozilla.org"], function([a1, a8]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ a1.uninstall();
+
+ do_check_neq(a8, null);
+ do_check_eq(a8.version, "1.0");
+ a8.uninstall();
+
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks doesn't update an add-on that is
+ // pending uninstall
+ let check_test_15;
+ add_test(function run_test_15() {
+ restartManager();
+
+ // Have an add-on there that will be updated so we see some events from it
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 8",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon8@tests.mozilla.org", function(a8) {
+ a8.uninstall();
+ do_check_false(hasFlag(a8.permissions, AddonManager.PERM_CAN_UPGRADE));
+
+ // The background update check will find updates for both add-ons but only
+ // proceed to install one of them.
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ let id = aInstall.existingAddon.id;
+ ok((id == "addon1@tests.mozilla.org" || id == "addon8@tests.mozilla.org"),
+ "Saw unexpected onNewInstall for " + id);
+ },
+
+ onDownloadStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadFailed: function(aInstall) {
+ ok(false, "Should not have seen onDownloadFailed event");
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ ok(false, "Should not have seen onDownloadCancelled event");
+ },
+
+ onInstallStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onInstallEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ do_execute_soon(check_test_15);
+ },
+
+ onInstallFailed: function(aInstall) {
+ ok(false, "Should not have seen onInstallFailed event");
+ },
+
+ onInstallCancelled: function(aInstall) {
+ ok(false, "Should not have seen onInstallCancelled event");
+ },
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+ });
+
+ check_test_15 = () => {
+ restartManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon8@tests.mozilla.org"], function([a1, a8]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ a1.uninstall();
+
+ do_check_eq(a8, null);
+
+ run_next_test();
+ });
+ }
+
+ add_test(function run_test_16() {
+ restartManager();
+
+ restartManager();
+
+ let url = "http://localhost:" + gPort + "/addons/test_install2_1.xpi";
+ AddonManager.getInstallForURL(url, function(aInstall) {
+ aInstall.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function install_2_1_ended() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a1) {
+ do_check_neq(a1.syncGUID, null);
+ let oldGUID = a1.syncGUID;
+
+ let url_2 = "http://localhost:" + gPort + "/addons/test_install2_2.xpi";
+ AddonManager.getInstallForURL(url_2, function(aInstall_2) {
+ aInstall_2.addListener({
+ onInstallEnded: function() {
+ do_execute_soon(function install_2_2_ended() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2.syncGUID, null);
+ do_check_eq(oldGUID, a2.syncGUID);
+
+ a2.uninstall();
+ run_next_test();
+ });
+ });
+ }
+ });
+ aInstall_2.install();
+ }, "application/x-xpinstall");
+ });
+ });
+ }
+ });
+ aInstall.install();
+ }, "application/x-xpinstall");
+ });
+
+ // Test that the update check correctly observes the
+ // extensions.strictCompatibility pref and compatibility overrides.
+ add_test(function run_test_17() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon9@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 9",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ equal(aInstall.existingAddon.id, "addon9@tests.mozilla.org",
+ "Saw unexpected onNewInstall for " + aInstall.existingAddon.id);
+ do_check_eq(aInstall.version, "3.0");
+ },
+ onDownloadFailed: function(aInstall) {
+ AddonManager.getAddonByID("addon9@tests.mozilla.org", function(a9) {
+ a9.uninstall();
+ run_next_test();
+ });
+ }
+ });
+
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS,
+ "http://localhost:" + gPort + "/data/test_update.xml");
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE,
+ "http://localhost:" + gPort + "/data/test_update.xml");
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+
+ // Tests that compatibility updates are applied to addons when the updated
+ // compatibility data wouldn't match with strict compatibility enabled.
+ add_test(function run_test_18() {
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "addon10@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 10",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon10@tests.mozilla.org", function(a10) {
+ do_check_neq(a10, null);
+
+ a10.findUpdates({
+ onNoCompatibilityUpdateAvailable: function() {
+ ok(false, "Should have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ a10.uninstall();
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ // Test that the update check correctly observes when an addon opts-in to
+ // strict compatibility checking.
+ add_test(function run_test_19() {
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "addon11@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 11",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon11@tests.mozilla.org", function(a11) {
+ do_check_neq(a11, null);
+
+ a11.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ ok(false, "Should have not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ a11.uninstall();
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ // Test that the update succeeds when the update.rdf URN contains a type prefix
+ // different from the add-on type
+ let continue_test_20;
+ add_test(function run_test_20() {
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "addon12@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 12",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({}, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], continue_test_20);
+
+ AddonManagerPrivate.backgroundUpdateCheck();
+ });
+
+ let check_test_20;
+ continue_test_20 = (install) => {
+ do_check_neq(install.existingAddon, null);
+ do_check_eq(install.existingAddon.id, "addon12@tests.mozilla.org");
+
+ prepare_test({
+ "addon12@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_20));
+ }
+
+ check_test_20 = (install) => {
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ restartManager();
+ AddonManager.getAddonByID("addon12@tests.mozilla.org", function(a12) {
+ do_check_neq(a12, null);
+ do_check_eq(a12.version, "2.0");
+ do_check_eq(a12.type, "extension");
+ a12.uninstall();
+
+ do_execute_soon(() => {
+ restartManager();
+ run_next_test()
+ });
+ });
+ }
+
+ add_task(function* cleanup() {
+ let addons = yield new Promise(resolve => {
+ AddonManager.getAddonsByTypes(["extension"], resolve);
+ });
+
+ for (let addon of addons)
+ addon.uninstall();
+
+ yield promiseRestartManager();
+
+ shutdownManager();
+
+ yield new Promise(do_execute_soon);
+ });
+}
+
+// Test that background update checks work for lightweight themes
+add_test(function run_test_7() {
+ startupManager();
+
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon.png",
+ updateURL: "http://localhost:" + gPort + "/data/lwtheme.js"
+ };
+
+ // XXX The lightweight theme manager strips non-https updateURLs so hack it
+ // back in.
+ let themes = JSON.parse(Services.prefs.getCharPref("lightweightThemes.usedThemes"));
+ do_check_eq(themes.length, 1);
+ themes[0].updateURL = "http://localhost:" + gPort + "/data/lwtheme.js";
+ Services.prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+
+ testserver.registerPathHandler("/data/lwtheme.js", function(request, response) {
+ // Server will specify an expiry in one year.
+ let expiry = new Date();
+ expiry.setFullYear(expiry.getFullYear() + 1);
+ response.setHeader("Expires", expiry.toUTCString(), false);
+ response.write(JSON.stringify({
+ id: "1",
+ version: "2",
+ name: "Updated Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index2.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon2.png",
+ updateURL: "http://localhost:" + gPort + "/data/lwtheme.js"
+ }));
+ });
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "1");
+ do_check_eq(p1.name, "Test LW Theme");
+ do_check_true(p1.isActive);
+ do_check_eq(p1.installDate.getTime(), p1.updateDate.getTime());
+
+ // 5 seconds leeway seems like a lot, but tests can run slow and really if
+ // this is within 5 seconds it is fine. If it is going to be wrong then it
+ // is likely to be hours out at least
+ do_check_true((Date.now() - p1.installDate.getTime()) < 5000);
+
+ gInstallDate = p1.installDate.getTime();
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onExternalInstall"
+ ], check_test_7);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+});
+
+function check_test_7() {
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "2");
+ do_check_eq(p1.name, "Updated Theme");
+ do_check_eq(p1.installDate.getTime(), gInstallDate);
+ do_check_true(p1.installDate.getTime() < p1.updateDate.getTime());
+
+ // 5 seconds leeway seems like a lot, but tests can run slow and really if
+ // this is within 5 seconds it is fine. If it is going to be wrong then it
+ // is likely to be hours out at least
+ do_check_true((Date.now() - p1.updateDate.getTime()) < 5000);
+
+ gInstallDate = p1.installDate.getTime();
+
+ run_next_test();
+ });
+}
+
+// Test that background update checks for lightweight themes do not use the cache
+// The update body from test 7 shouldn't be used since the cache should be bypassed.
+add_test(function () {
+ // XXX The lightweight theme manager strips non-https updateURLs so hack it
+ // back in.
+ let themes = JSON.parse(Services.prefs.getCharPref("lightweightThemes.usedThemes"));
+ do_check_eq(themes.length, 1);
+ themes[0].updateURL = "http://localhost:" + gPort + "/data/lwtheme.js";
+ Services.prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+
+ testserver.registerPathHandler("/data/lwtheme.js", function(request, response) {
+ response.write(JSON.stringify({
+ id: "1",
+ version: "3",
+ name: "Updated Theme v.3",
+ description: "A test theme v.3",
+ author: "John Smith",
+ homepageURL: "http://localhost:" + gPort + "/data/index3.html?v=3",
+ headerURL: "http://localhost:" + gPort + "/data/header.png?v=3",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png?v=3",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png?v=3",
+ iconURL: "http://localhost:" + gPort + "/data/icon2.png?v=3",
+ updateURL: "https://localhost:" + gPort + "/data/lwtheme.js?v=3"
+ }));
+ });
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "2");
+ do_check_eq(p1.name, "Updated Theme");
+ do_check_true(p1.isActive);
+ do_check_eq(p1.installDate.getTime(), gInstallDate);
+ do_check_true(p1.installDate.getTime() < p1.updateDate.getTime());
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onExternalInstall"
+ ], check_test_7_cache);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+});
+
+function check_test_7_cache() {
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ let currentTheme = LightweightThemeManager.currentTheme;
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "3");
+ do_check_eq(p1.name, "Updated Theme v.3");
+ do_check_eq(p1.description, "A test theme v.3");
+ do_print(JSON.stringify(p1));
+ do_check_eq(p1.creator.name, "John Smith");
+ do_check_eq(p1.homepageURL, "http://localhost:" + gPort + "/data/index3.html?v=3");
+ do_check_eq(p1.screenshots[0].url, "http://localhost:" + gPort + "/data/preview.png?v=3");
+ do_check_eq(p1.iconURL, "http://localhost:" + gPort + "/data/icon2.png?v=3");
+ do_check_eq(currentTheme.headerURL, "http://localhost:" + gPort + "/data/header.png?v=3");
+ do_check_eq(currentTheme.footerURL, "http://localhost:" + gPort + "/data/footer.png?v=3");
+ do_check_eq(currentTheme.updateURL, "https://localhost:" + gPort + "/data/lwtheme.js?v=3");
+
+ do_check_eq(p1.installDate.getTime(), gInstallDate);
+ do_check_true(p1.installDate.getTime() < p1.updateDate.getTime());
+
+ run_next_test();
+ });
+}
+
+// Test that the update check returns nothing for addons in locked install
+// locations.
+add_test(function run_test_locked_install() {
+ const lockedDir = gProfD.clone();
+ lockedDir.append("locked_extensions");
+ registerDirectory("XREAppFeat", lockedDir);
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "addon13@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_update.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 13",
+ }, lockedDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon13@tests.mozilla.org", function(a13) {
+ do_check_neq(a13, null);
+
+ a13.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ ok(false, "Should have not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ ok(true, "Should have seen an onUpdateFinished");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+
+ AddonManager.getAllInstalls(aInstalls => {
+ do_check_eq(aInstalls.length, 0);
+ });
+ run_next_test();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js
new file mode 100644
index 000000000..b50fd4a55
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateCancel.js
@@ -0,0 +1,138 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// Test cancelling add-on update checks while in progress (bug 925389)
+
+Components.utils.import("resource://gre/modules/Promise.jsm");
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+// Set up an HTTP server to respond to update requests
+Components.utils.import("resource://testing-common/httpd.js");
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+function run_test() {
+ // Kick off the task-based tests...
+ run_next_test();
+}
+
+// Install one extension
+// Start download of update check (but delay HTTP response)
+// Cancel update check
+// - ensure we get cancel notification
+// complete HTTP response
+// - ensure no callbacks after cancel
+// - ensure update is gone
+
+// Create an addon update listener containing a promise
+// that resolves when the update is cancelled
+function makeCancelListener() {
+ let updated = Promise.defer();
+ return {
+ onUpdateAvailable: function(addon, install) {
+ updated.reject("Should not have seen onUpdateAvailable notification");
+ },
+
+ onUpdateFinished: function(aAddon, aError) {
+ do_print("onUpdateCheckFinished: " + aAddon.id + " " + aError);
+ updated.resolve(aError);
+ },
+ promise: updated.promise
+ };
+}
+
+// Set up the HTTP server so that we can control when it responds
+var httpReceived = Promise.defer();
+function dataHandler(aRequest, aResponse) {
+ aResponse.processAsync();
+ httpReceived.resolve([aRequest, aResponse]);
+}
+var testserver = new HttpServer();
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+testserver.registerPathHandler("/data/test_update.rdf", dataHandler);
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+
+// Set up an add-on for update check
+writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_update.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+}, profileDir);
+
+add_task(function* cancel_during_check() {
+ startupManager();
+
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+ do_check_neq(a1, null);
+
+ let listener = makeCancelListener();
+ a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+
+ // Wait for the http request to arrive
+ let [request, response] = yield httpReceived.promise;
+
+ // cancelUpdate returns true if there is an update check in progress
+ do_check_true(a1.cancelUpdate());
+
+ let updateResult = yield listener.promise;
+ do_check_eq(AddonManager.UPDATE_STATUS_CANCELLED, updateResult);
+
+ // Now complete the HTTP request
+ let file = do_get_cwd();
+ file.append("data");
+ file.append("test_update.rdf");
+ let data = loadFile(file);
+ response.write(data);
+ response.finish();
+
+ // trying to cancel again should return false, i.e. nothing to cancel
+ do_check_false(a1.cancelUpdate());
+
+ yield true;
+});
+
+// Test that update check is cancelled if the XPI provider shuts down while
+// the update check is in progress
+add_task(function* shutdown_during_check() {
+ // Reset our HTTP listener
+ httpReceived = Promise.defer();
+
+ let a1 = yield promiseAddonByID("addon1@tests.mozilla.org");
+ do_check_neq(a1, null);
+
+ let listener = makeCancelListener();
+ a1.findUpdates(listener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+
+ // Wait for the http request to arrive
+ let [request, response] = yield httpReceived.promise;
+
+ shutdownManager();
+
+ let updateResult = yield listener.promise;
+ do_check_eq(AddonManager.UPDATE_STATUS_CANCELLED, updateResult);
+
+ // Now complete the HTTP request
+ let file = do_get_cwd();
+ file.append("data");
+ file.append("test_update.rdf");
+ let data = loadFile(file);
+ response.write(data);
+ response.finish();
+
+ yield testserver.stop(Promise.defer().resolve);
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js
new file mode 100644
index 000000000..a1d872009
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_compatmode.js
@@ -0,0 +1,184 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-on update check correctly fills in the
+// %COMPATIBILITY_MODE% token in the update URL.
+
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+Components.utils.import("resource://testing-common/httpd.js");
+var testserver = new HttpServer();
+testserver.start(-1);
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_updatecompatmode_ignore.rdf", testserver);
+mapFile("/data/test_updatecompatmode_normal.rdf", testserver);
+mapFile("/data/test_updatecompatmode_strict.rdf", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ do_test_pending();
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ writeInstallRDFForExtension({
+ id: "compatmode-normal@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_updatecompatmode_%COMPATIBILITY_MODE%.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon - normal"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "compatmode-strict@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_updatecompatmode_%COMPATIBILITY_MODE%.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon - strict"
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "compatmode-strict-optin@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_updatecompatmode_%COMPATIBILITY_MODE%.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon - strict opt-in",
+ strictCompatibility: true
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "compatmode-ignore@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/test_updatecompatmode_%COMPATIBILITY_MODE%.rdf",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon - ignore",
+ }, profileDir);
+
+ startupManager();
+ run_test_1();
+}
+
+function end_test() {
+ testserver.stop(do_test_finished);
+}
+
+
+// Strict compatibility checking disabled.
+function run_test_1() {
+ do_print("Testing with strict compatibility checking disabled");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+ AddonManager.getAddonByID("compatmode-normal@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ addon.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ do_throw("Should have not have seen compatibility information");
+ },
+
+ onNoUpdateAvailable: function() {
+ do_throw("Should have seen an available update");
+ },
+
+ onUpdateAvailable: function(unused, install) {
+ do_check_eq(install.version, "2.0")
+ },
+
+ onUpdateFinished: function() {
+ run_test_2();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+// Strict compatibility checking enabled.
+function run_test_2() {
+ do_print("Testing with strict compatibility checking enabled");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+ AddonManager.getAddonByID("compatmode-strict@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ addon.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ do_throw("Should have not have seen compatibility information");
+ },
+
+ onNoUpdateAvailable: function() {
+ do_throw("Should have seen an available update");
+ },
+
+ onUpdateAvailable: function(unused, install) {
+ do_check_eq(install.version, "2.0")
+ },
+
+ onUpdateFinished: function() {
+ run_test_3();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+// Strict compatibility checking opt-in.
+function run_test_3() {
+ do_print("Testing with strict compatibility disabled, but addon opt-in");
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+ AddonManager.getAddonByID("compatmode-strict-optin@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ addon.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ do_throw("Should have not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ do_throw("Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ run_test_4();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
+
+// Compatibility checking disabled.
+function run_test_4() {
+ do_print("Testing with all compatibility checking disabled");
+ AddonManager.checkCompatibility = false;
+ AddonManager.getAddonByID("compatmode-ignore@tests.mozilla.org", function(addon) {
+ do_check_neq(addon, null);
+ addon.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ do_throw("Should have not have seen compatibility information");
+ },
+
+ onNoUpdateAvailable: function() {
+ do_throw("Should have seen an available update");
+ },
+
+ onUpdateAvailable: function(unused, install) {
+ do_check_eq(install.version, "2.0")
+ },
+
+ onUpdateFinished: function() {
+ end_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js
new file mode 100644
index 000000000..e899619d6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_ignorecompat.js
@@ -0,0 +1,107 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-on update checks work correctly when compatibility
+// check is disabled.
+
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+var testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_update.rdf", testserver);
+mapFile("/data/test_update.json", testserver);
+mapFile("/data/test_update.xml", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+let testParams = [
+ { updateFile: "test_update.rdf",
+ appId: "xpcshell@tests.mozilla.org" },
+ { updateFile: "test_update.json",
+ appId: "toolkit@mozilla.org" },
+];
+
+for (let test of testParams) {
+ let { updateFile, appId } = test;
+
+ // Test that the update check correctly observes the
+ // extensions.strictCompatibility pref and compatibility overrides.
+ add_test(function () {
+ writeInstallRDFForExtension({
+ id: "addon9@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 9",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ if (aInstall.existingAddon.id != "addon9@tests.mozilla.org")
+ do_throw("Saw unexpected onNewInstall for " + aInstall.existingAddon.id);
+ do_check_eq(aInstall.version, "4.0");
+ },
+ onDownloadFailed: function(aInstall) {
+ run_next_test();
+ }
+ });
+
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE,
+ "http://localhost:" + gPort + "/data/" + updateFile);
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+
+ // Test that the update check correctly observes when an addon opts-in to
+ // strict compatibility checking.
+ add_test(function () {
+ writeInstallRDFForExtension({
+ id: "addon11@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 11",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon11@tests.mozilla.org", function(a11) {
+ do_check_neq(a11, null);
+
+ a11.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ do_throw("Should not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ do_throw("Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
new file mode 100644
index 000000000..2b1f65a9f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_strictcompat.js
@@ -0,0 +1,1126 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that add-on update checks work
+
+const PREF_MATCH_OS_LOCALE = "intl.locale.matchOS";
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+const PREF_GETADDONS_CACHE_ENABLED = "extensions.getAddons.cache.enabled";
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+// This test requires lightweight themes update to be enabled even if the app
+// doesn't support lightweight themes.
+Services.prefs.setBoolPref("lightweightThemes.update.enabled", true);
+
+Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm");
+
+const PARAMS = "?%REQ_VERSION%/%ITEM_ID%/%ITEM_VERSION%/%ITEM_MAXAPPVERSION%/" +
+ "%ITEM_STATUS%/%APP_ID%/%APP_VERSION%/%CURRENT_APP_VERSION%/" +
+ "%APP_OS%/%APP_ABI%/%APP_LOCALE%/%UPDATE_TYPE%";
+
+var gInstallDate;
+
+var testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+mapFile("/data/test_update.rdf", testserver);
+mapFile("/data/test_update.json", testserver);
+mapFile("/data/test_update.xml", testserver);
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ Services.prefs.setBoolPref(PREF_MATCH_OS_LOCALE, false);
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+
+ run_next_test();
+}
+
+let testParams = [
+ { updateFile: "test_update.rdf",
+ appId: "xpcshell@tests.mozilla.org" },
+ { updateFile: "test_update.json",
+ appId: "toolkit@mozilla.org" },
+];
+
+for (let test of testParams) {
+ let { updateFile, appId } = test;
+
+ add_test(function() {
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }],
+ name: "Test Addon 2",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "5",
+ maxVersion: "5"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ startupManager();
+
+ run_next_test();
+ });
+
+ // Verify that an update is available and can be installed.
+ let check_test_1;
+ add_test(function run_test_1() {
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "1.0");
+ do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DEFAULT);
+ do_check_eq(a1.releaseNotesURI, null);
+
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DEFAULT;
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ ["onPropertyChanged", ["applyBackgroundUpdates"]]
+ ]
+ });
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+ check_test_completed();
+
+ a1.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ prepare_test({}, [
+ "onNewInstall",
+ ]);
+
+ a1.findUpdates({
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ensure_test_completed();
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], install);
+
+ do_check_eq(addon, a1);
+ do_check_eq(install.name, addon.name);
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.existingAddon, addon);
+ do_check_eq(install.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+
+ // Verify that another update check returns the same AddonInstall
+ a1.findUpdates({
+ onNoCompatibilityUpdateAvailable: function() {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function(newAddon, newInstall) {
+ AddonManager.getAllInstalls(function(aInstalls2) {
+ do_check_eq(aInstalls2.length, 1);
+ do_check_eq(aInstalls2[0], install);
+ do_check_eq(newAddon, addon);
+ do_check_eq(newInstall, install);
+
+ prepare_test({}, [
+ "onDownloadStarted",
+ "onDownloadEnded",
+ ], check_test_1);
+ install.install();
+ });
+ },
+
+ onNoUpdateAvailable: function() {
+ ok(false, "Should not have seen onNoUpdateAvailable notification");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoUpdateAvailable notification");
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ let run_test_2;
+ check_test_1 = (install) => {
+ ensure_test_completed();
+ do_check_eq(install.state, AddonManager.STATE_DOWNLOADED);
+ run_test_2(install);
+ return false;
+ };
+
+ // Continue installing the update.
+ let check_test_2;
+ run_test_2 = (install) => {
+ // Verify that another update check returns no new update
+ install.existingAddon.findUpdates({
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen onNoCompatibilityUpdateAvailable notification");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should find no available update when one is already downloading");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ AddonManager.getAllInstalls(function(aInstalls) {
+ do_check_eq(aInstalls.length, 1);
+ do_check_eq(aInstalls[0], install);
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], check_test_2);
+ install.install();
+ });
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ };
+
+ check_test_2 = () => {
+ ensure_test_completed();
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", callback_soon(function(olda1) {
+ do_check_neq(olda1, null);
+ do_check_eq(olda1.version, "1.0");
+ do_check_true(isExtensionInAddonsList(profileDir, olda1.id));
+
+ shutdownManager();
+
+ startupManager();
+
+ do_check_true(isExtensionInAddonsList(profileDir, "addon1@tests.mozilla.org"));
+
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+ do_check_eq(a1.applyBackgroundUpdates, AddonManager.AUTOUPDATE_DISABLE);
+ do_check_eq(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+
+ a1.uninstall();
+ run_next_test();
+ });
+ }));
+ };
+
+
+ // Check that an update check finds compatibility updates and applies them
+ let check_test_3;
+ add_test(function run_test_3() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_false(a2.isActive);
+ do_check_false(a2.isCompatible);
+ do_check_true(a2.appDisabled);
+ do_check_true(a2.isCompatibleWith("0", "0"));
+
+ a2.findUpdates({
+ onCompatibilityUpdateAvailable: function(addon) {
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ do_check_false(a2.isActive);
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_eq(addon, a2);
+ do_execute_soon(check_test_3);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ check_test_3 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", function(a2) {
+ do_check_neq(a2, null);
+ do_check_true(a2.isActive);
+ do_check_true(a2.isCompatible);
+ do_check_false(a2.appDisabled);
+ a2.uninstall();
+
+ run_next_test();
+ });
+ }
+
+ // Checks that we see no compatibility information when there is none.
+ add_test(function run_test_4() {
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_true(a3.isCompatibleWith("5", "5"));
+ do_check_false(a3.isCompatibleWith("2", "2"));
+
+ a3.findUpdates({
+ sawUpdate: false,
+ onCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen compatibility information");
+ },
+
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ this.sawUpdate = true;
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_true(this.sawUpdate);
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ // Checks that compatibility info for future apps are detected but don't make
+ // the item compatibile.
+ let check_test_5;
+ add_test(function run_test_5() {
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_true(a3.isCompatibleWith("5", "5"));
+ do_check_false(a3.isCompatibleWith("2", "2"));
+
+ a3.findUpdates({
+ sawUpdate: false,
+ onCompatibilityUpdateAvailable: function(addon) {
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+ do_check_false(a3.isActive);
+ this.sawUpdate = true;
+ },
+
+ onNoCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should have seen some compatibility information");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onNoUpdateAvailable: function(addon) {
+ do_check_true(this.sawUpdate);
+ do_execute_soon(check_test_5);
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED, "3.0", "3.0");
+ });
+ });
+
+ check_test_5 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon3@tests.mozilla.org", function(a3) {
+ do_check_neq(a3, null);
+ do_check_false(a3.isActive);
+ do_check_false(a3.isCompatible);
+ do_check_true(a3.appDisabled);
+
+ a3.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks work
+ let continue_test_6;
+ add_test(function run_test_6() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+ restartManager();
+
+ prepare_test({}, [
+ "onNewInstall",
+ "onDownloadStarted",
+ "onDownloadEnded"
+ ], continue_test_6);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+
+ let check_test_6;
+ continue_test_6 = (install) => {
+ do_check_neq(install.existingAddon, null);
+ do_check_eq(install.existingAddon.id, "addon1@tests.mozilla.org");
+
+ prepare_test({
+ "addon1@tests.mozilla.org": [
+ "onInstalling"
+ ]
+ }, [
+ "onInstallStarted",
+ "onInstallEnded",
+ ], callback_soon(check_test_6));
+ }
+
+ check_test_6 = (install) => {
+ do_check_eq(install.existingAddon.pendingUpgrade.install, install);
+
+ restartManager();
+ AddonManager.getAddonByID("addon1@tests.mozilla.org", function(a1) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ do_check_eq(a1.releaseNotesURI.spec, "http://example.com/updateInfo.xhtml");
+ a1.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Verify the parameter escaping in update urls.
+ add_test(function run_test_8() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "5.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "67.0.5b1",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: "toolkit@mozilla.org",
+ minVersion: "0",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 2",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.3+",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }, {
+ id: "toolkit@mozilla.org",
+ minVersion: "0",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "0.5ab6",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "5"
+ }],
+ name: "Test Addon 4",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon5@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 5",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon6@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/param_test.rdf" + PARAMS,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 6",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon2@tests.mozilla.org", callback_soon(function(a2) {
+ a2.userDisabled = true;
+ restartManager();
+
+ testserver.registerPathHandler("/data/param_test.rdf", function(request, response) {
+ do_check_neq(request.queryString, "");
+ let [req_version, item_id, item_version,
+ item_maxappversion, item_status,
+ app_id, app_version, current_app_version,
+ app_os, app_abi, app_locale, update_type] =
+ request.queryString.split("/").map(a => decodeURIComponent(a));
+
+ do_check_eq(req_version, "2");
+
+ switch (item_id) {
+ case "addon1@tests.mozilla.org":
+ do_check_eq(item_version, "5.0");
+ do_check_eq(item_maxappversion, "2");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "97");
+ break;
+ case "addon2@tests.mozilla.org":
+ do_check_eq(item_version, "67.0.5b1");
+ do_check_eq(item_maxappversion, "3");
+ do_check_eq(item_status, "userDisabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "49");
+ break;
+ case "addon3@tests.mozilla.org":
+ do_check_eq(item_version, "1.3+");
+ do_check_eq(item_maxappversion, "0");
+ do_check_eq(item_status, "userEnabled,incompatible");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "112");
+ break;
+ case "addon4@tests.mozilla.org":
+ do_check_eq(item_version, "0.5ab6");
+ do_check_eq(item_maxappversion, "5");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "2");
+ do_check_eq(update_type, "98");
+ break;
+ case "addon5@tests.mozilla.org":
+ do_check_eq(item_version, "1.0");
+ do_check_eq(item_maxappversion, "1");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "35");
+ break;
+ case "addon6@tests.mozilla.org":
+ do_check_eq(item_version, "1.0");
+ do_check_eq(item_maxappversion, "1");
+ do_check_eq(item_status, "userEnabled");
+ do_check_eq(app_version, "1");
+ do_check_eq(update_type, "99");
+ break;
+ default:
+ ok(false, "Update request for unexpected add-on " + item_id);
+ }
+
+ do_check_eq(app_id, "xpcshell@tests.mozilla.org");
+ do_check_eq(current_app_version, "1");
+ do_check_eq(app_os, "XPCShell");
+ do_check_eq(app_abi, "noarch-spidermonkey");
+ do_check_eq(app_locale, "fr-FR");
+
+ request.setStatusLine(null, 500, "Server Error");
+ });
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org",
+ "addon5@tests.mozilla.org",
+ "addon6@tests.mozilla.org"],
+ function([a1_2, a2_2, a3_2, a4_2, a5_2, a6_2]) {
+ let count = 6;
+
+ function next_test() {
+ a1_2.uninstall();
+ a2_2.uninstall();
+ a3_2.uninstall();
+ a4_2.uninstall();
+ a5_2.uninstall();
+ a6_2.uninstall();
+
+ restartManager();
+ run_next_test();
+ }
+
+ let compatListener = {
+ onUpdateFinished: function(addon, error) {
+ if (--count == 0)
+ do_execute_soon(next_test);
+ }
+ };
+
+ let updateListener = {
+ onUpdateAvailable: function(addon, update) {
+ // Dummy so the update checker knows we care about new versions
+ },
+
+ onUpdateFinished: function(addon, error) {
+ if (--count == 0)
+ do_execute_soon(next_test);
+ }
+ };
+
+ a1_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ a2_2.findUpdates(compatListener, AddonManager.UPDATE_WHEN_ADDON_INSTALLED);
+ a3_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ a4_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, "2");
+ a5_2.findUpdates(compatListener, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ a6_2.findUpdates(updateListener, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ });
+ }));
+ });
+
+ // Tests that if an install.rdf claims compatibility then the add-on will be
+ // seen as compatible regardless of what the update.rdf says.
+ add_test(function run_test_9() {
+ writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "5.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ restartManager();
+
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ do_check_true(a4.isActive, "addon4 is active");
+ do_check_true(a4.isCompatible, "addon4 is compatible");
+
+ run_next_test();
+ });
+ });
+
+ // Tests that a normal update check won't decrease a targetApplication's
+ // maxVersion.
+ add_test(function run_test_10() {
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ a4.findUpdates({
+ onUpdateFinished: function(addon) {
+ do_check_true(addon.isCompatible, "addon4 is compatible");
+
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_PERIODIC_UPDATE);
+ });
+ });
+
+ // Tests that an update check for a new application will decrease a
+ // targetApplication's maxVersion.
+ add_test(function run_test_11() {
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ a4.findUpdates({
+ onUpdateFinished: function(addon) {
+ do_check_false(addon.isCompatible, "addon4 is compatible");
+
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_NEW_APP_INSTALLED);
+ });
+ });
+
+ // Check that the decreased maxVersion applied and disables the add-on
+ add_test(function run_test_12() {
+ restartManager();
+
+ AddonManager.getAddonByID("addon4@tests.mozilla.org", function(a4) {
+ do_check_false(a4.isActive, "addon4 is active");
+ do_check_false(a4.isCompatible, "addon4 is compatible");
+
+ a4.uninstall();
+ run_next_test();
+ });
+ });
+
+ // Tests that no compatibility update is passed to the listener when there is
+ // compatibility info for the current version of the app but not for the
+ // version of the app that the caller requested an update check for.
+ let check_test_13;
+ add_test(function run_test_13() {
+ restartManager();
+
+ // Not initially compatible but the update check will make it compatible
+ writeInstallRDFForExtension({
+ id: "addon7@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0",
+ maxVersion: "0"
+ }],
+ name: "Test Addon 7",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon7@tests.mozilla.org", function(a7) {
+ do_check_neq(a7, null);
+ do_check_false(a7.isActive);
+ do_check_false(a7.isCompatible);
+ do_check_true(a7.appDisabled);
+ do_check_true(a7.isCompatibleWith("0", "0"));
+
+ a7.findUpdates({
+ sawUpdate: false,
+ onCompatibilityUpdateAvailable: function(addon) {
+ ok(false, "Should not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function(addon, install) {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function(addon) {
+ do_check_true(addon.isCompatible);
+ do_execute_soon(check_test_13);
+ }
+ }, AddonManager.UPDATE_WHEN_NEW_APP_DETECTED, "3.0", "3.0");
+ });
+ });
+
+ check_test_13 = () => {
+ restartManager();
+ AddonManager.getAddonByID("addon7@tests.mozilla.org", function(a7) {
+ do_check_neq(a7, null);
+ do_check_true(a7.isActive);
+ do_check_true(a7.isCompatible);
+ do_check_false(a7.appDisabled);
+
+ a7.uninstall();
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks doesn't update an add-on that isn't
+ // allowed to update automatically.
+ let check_test_14;
+ add_test(function run_test_14() {
+ restartManager();
+
+ // Have an add-on there that will be updated so we see some events from it
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 8",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon8@tests.mozilla.org", function(a8) {
+ a8.applyBackgroundUpdates = AddonManager.AUTOUPDATE_DISABLE;
+
+ // The background update check will find updates for both add-ons but only
+ // proceed to install one of them.
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ let id = aInstall.existingAddon.id;
+ ok((id == "addon1@tests.mozilla.org" || id == "addon8@tests.mozilla.org"),
+ "Saw unexpected onNewInstall for " + id);
+ },
+
+ onDownloadStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadFailed: function(aInstall) {
+ ok(false, "Should not have seen onDownloadFailed event");
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ ok(false, "Should not have seen onDownloadCancelled event");
+ },
+
+ onInstallStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onInstallEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ do_check_eq(aInstall.existingAddon.pendingUpgrade.install, aInstall);
+
+ do_execute_soon(check_test_14);
+ },
+
+ onInstallFailed: function(aInstall) {
+ ok(false, "Should not have seen onInstallFailed event");
+ },
+
+ onInstallCancelled: function(aInstall) {
+ ok(false, "Should not have seen onInstallCancelled event");
+ },
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+ });
+
+ check_test_14 = () => {
+ restartManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon8@tests.mozilla.org"], function([a1, a8]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ a1.uninstall();
+
+ do_check_neq(a8, null);
+ do_check_eq(a8.version, "1.0");
+ a8.uninstall();
+
+ run_next_test();
+ });
+ }
+
+ // Test that background update checks doesn't update an add-on that is
+ // pending uninstall
+ let check_test_15;
+ add_test(function run_test_15() {
+ restartManager();
+
+ // Have an add-on there that will be updated so we see some events from it
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ }, profileDir);
+
+ writeInstallRDFForExtension({
+ id: "addon8@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 8",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon8@tests.mozilla.org", function(a8) {
+ a8.uninstall();
+ do_check_false(hasFlag(a8.permissions, AddonManager.PERM_CAN_UPGRADE));
+
+ // The background update check will find updates for both add-ons but only
+ // proceed to install one of them.
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ let id = aInstall.existingAddon.id;
+ ok((id == "addon1@tests.mozilla.org" || id == "addon8@tests.mozilla.org"),
+ "Saw unexpected onNewInstall for " + id);
+ },
+
+ onDownloadStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onDownloadFailed: function(aInstall) {
+ ok(false, "Should not have seen onDownloadFailed event");
+ },
+
+ onDownloadCancelled: function(aInstall) {
+ ok(false, "Should not have seen onDownloadCancelled event");
+ },
+
+ onInstallStarted: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ },
+
+ onInstallEnded: function(aInstall) {
+ do_check_eq(aInstall.existingAddon.id, "addon1@tests.mozilla.org");
+ do_execute_soon(check_test_15);
+ },
+
+ onInstallFailed: function(aInstall) {
+ ok(false, "Should not have seen onInstallFailed event");
+ },
+
+ onInstallCancelled: function(aInstall) {
+ ok(false, "Should not have seen onInstallCancelled event");
+ },
+ });
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+ });
+
+ check_test_15 = () => {
+ restartManager();
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon8@tests.mozilla.org"], function([a1, a8]) {
+ do_check_neq(a1, null);
+ do_check_eq(a1.version, "2.0");
+ a1.uninstall();
+
+ do_check_eq(a8, null);
+
+ run_next_test();
+ });
+ }
+
+ // Test that the update check correctly observes the
+ // extensions.strictCompatibility pref and compatibility overrides.
+ add_test(function run_test_17() {
+ restartManager();
+
+ writeInstallRDFForExtension({
+ id: "addon9@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 9",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.addInstallListener({
+ onNewInstall: function(aInstall) {
+ equal(aInstall.existingAddon.id, "addon9@tests.mozilla.org",
+ "Saw unexpected onNewInstall for " + aInstall.existingAddon.id);
+ do_check_eq(aInstall.version, "2.0");
+ },
+ onDownloadFailed: function(aInstall) {
+ AddonManager.getAddonByID("addon9@tests.mozilla.org", function(a9) {
+ a9.uninstall();
+ run_next_test();
+ });
+ }
+ });
+
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS,
+ "http://localhost:" + gPort + "/data/test_update.xml");
+ Services.prefs.setCharPref(PREF_GETADDONS_BYIDS_PERFORMANCE,
+ "http://localhost:" + gPort + "/data/test_update.xml");
+ Services.prefs.setBoolPref(PREF_GETADDONS_CACHE_ENABLED, true);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+
+ // Test that the update check correctly observes when an addon opts-in to
+ // strict compatibility checking.
+ add_test(function run_test_19() {
+ restartManager();
+ writeInstallRDFForExtension({
+ id: "addon11@tests.mozilla.org",
+ version: "1.0",
+ updateURL: "http://localhost:" + gPort + "/data/" + updateFile,
+ targetApplications: [{
+ id: appId,
+ minVersion: "0.1",
+ maxVersion: "0.2"
+ }],
+ name: "Test Addon 11",
+ }, profileDir);
+ restartManager();
+
+ AddonManager.getAddonByID("addon11@tests.mozilla.org", function(a11) {
+ do_check_neq(a11, null);
+
+ a11.findUpdates({
+ onCompatibilityUpdateAvailable: function() {
+ ok(false, "Should have not have seen compatibility information");
+ },
+
+ onUpdateAvailable: function() {
+ ok(false, "Should not have seen an available update");
+ },
+
+ onUpdateFinished: function() {
+ run_next_test();
+ }
+ }, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ });
+ });
+
+ add_task(function* cleanup() {
+ let addons = yield new Promise(resolve => {
+ AddonManager.getAddonsByTypes(["extension"], resolve);
+ });
+
+ for (let addon of addons)
+ addon.uninstall();
+
+ yield promiseRestartManager();
+
+ shutdownManager();
+
+ yield new Promise(do_execute_soon);
+ });
+}
+
+// Test that background update checks work for lightweight themes
+add_test(function run_test_7() {
+ startupManager();
+
+ LightweightThemeManager.currentTheme = {
+ id: "1",
+ version: "1",
+ name: "Test LW Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon.png",
+ updateURL: "http://localhost:" + gPort + "/data/lwtheme.js"
+ };
+
+ // XXX The lightweight theme manager strips non-https updateURLs so hack it
+ // back in.
+ let themes = JSON.parse(Services.prefs.getCharPref("lightweightThemes.usedThemes"));
+ do_check_eq(themes.length, 1);
+ themes[0].updateURL = "http://localhost:" + gPort + "/data/lwtheme.js";
+ Services.prefs.setCharPref("lightweightThemes.usedThemes", JSON.stringify(themes));
+
+ testserver.registerPathHandler("/data/lwtheme.js", function(request, response) {
+ response.write(JSON.stringify({
+ id: "1",
+ version: "2",
+ name: "Updated Theme",
+ description: "A test theme",
+ author: "Mozilla",
+ homepageURL: "http://localhost:" + gPort + "/data/index2.html",
+ headerURL: "http://localhost:" + gPort + "/data/header.png",
+ footerURL: "http://localhost:" + gPort + "/data/footer.png",
+ previewURL: "http://localhost:" + gPort + "/data/preview.png",
+ iconURL: "http://localhost:" + gPort + "/data/icon2.png",
+ updateURL: "http://localhost:" + gPort + "/data/lwtheme.js"
+ }));
+ });
+
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "1");
+ do_check_eq(p1.name, "Test LW Theme");
+ do_check_true(p1.isActive);
+ do_check_eq(p1.installDate.getTime(), p1.updateDate.getTime());
+
+ // 5 seconds leeway seems like a lot, but tests can run slow and really if
+ // this is within 5 seconds it is fine. If it is going to be wrong then it
+ // is likely to be hours out at least
+ do_check_true((Date.now() - p1.installDate.getTime()) < 5000);
+
+ gInstallDate = p1.installDate.getTime();
+
+ prepare_test({
+ "1@personas.mozilla.org": [
+ ["onInstalling", false],
+ "onInstalled"
+ ]
+ }, [
+ "onExternalInstall"
+ ], check_test_7);
+
+ AddonManagerInternal.backgroundUpdateCheck();
+ });
+});
+
+function check_test_7() {
+ AddonManager.getAddonByID("1@personas.mozilla.org", function(p1) {
+ do_check_neq(p1, null);
+ do_check_eq(p1.version, "2");
+ do_check_eq(p1.name, "Updated Theme");
+ do_check_eq(p1.installDate.getTime(), gInstallDate);
+ do_check_true(p1.installDate.getTime() < p1.updateDate.getTime());
+
+ // 5 seconds leeway seems like a lot, but tests can run slow and really if
+ // this is within 5 seconds it is fine. If it is going to be wrong then it
+ // is likely to be hours out at least
+ do_check_true((Date.now() - p1.updateDate.getTime()) < 5000);
+
+ gInstallDate = p1.installDate.getTime();
+
+ run_next_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
new file mode 100644
index 000000000..ff5d5d321
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
@@ -0,0 +1,248 @@
+"use strict";
+
+const TOOLKIT_ID = "toolkit@mozilla.org";
+
+// We don't have an easy way to serve update manifests from a secure URL.
+Services.prefs.setBoolPref(PREF_EM_CHECK_UPDATE_SECURITY, false);
+
+var testserver = createHttpServer();
+gPort = testserver.identity.primaryPort;
+
+const uuidGenerator = AM_Cc["@mozilla.org/uuid-generator;1"].getService(AM_Ci.nsIUUIDGenerator);
+
+const extensionsDir = gProfD.clone();
+extensionsDir.append("extensions");
+
+const addonsDir = gTmpD.clone();
+addonsDir.append("addons");
+addonsDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, 0o755);
+
+do_register_cleanup(() => addonsDir.remove(true));
+
+testserver.registerDirectory("/addons/", addonsDir);
+
+
+let gUpdateManifests = {};
+
+function mapManifest(aPath, aManifestData) {
+ gUpdateManifests[aPath] = aManifestData;
+ testserver.registerPathHandler(aPath, serveManifest);
+}
+
+function serveManifest(request, response) {
+ let manifest = gUpdateManifests[request.path];
+
+ response.setHeader("Content-Type", manifest.contentType, false);
+ response.write(manifest.data);
+}
+
+
+function promiseInstallWebExtension(aData) {
+ let addonFile = createTempWebExtensionFile(aData);
+
+ return promiseInstallAllFiles([addonFile]).then(() => {
+ Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
+ return promiseAddonByID(aData.id);
+ });
+}
+
+var checkUpdates = Task.async(function* (aData, aReason = AddonManager.UPDATE_WHEN_PERIODIC_UPDATE) {
+ function provide(obj, path, value) {
+ path = path.split(".");
+ let prop = path.pop();
+
+ for (let key of path) {
+ if (!(key in obj))
+ obj[key] = {};
+ obj = obj[key];
+ }
+
+ if (!(prop in obj))
+ obj[prop] = value;
+ }
+
+ let id = uuidGenerator.generateUUID().number;
+ provide(aData, "addon.id", id);
+ provide(aData, "addon.manifest.applications.gecko.id", id);
+
+ let updatePath = `/updates/${id}.json`.replace(/[{}]/g, "");
+ let updateUrl = `http://localhost:${gPort}${updatePath}`
+
+ let addonData = { updates: [] };
+ let manifestJSON = {
+ addons: { [id]: addonData }
+ };
+
+
+ provide(aData, "addon.manifest.applications.gecko.update_url", updateUrl);
+ let awaitInstall = promiseInstallWebExtension(aData.addon);
+
+
+ for (let version of Object.keys(aData.updates)) {
+ let update = aData.updates[version];
+ update.version = version;
+
+ provide(update, "addon.id", id);
+ provide(update, "addon.manifest.applications.gecko.id", id);
+ let addon = update.addon;
+
+ delete update.addon;
+
+ let file;
+ if (addon.rdf) {
+ provide(addon, "version", version);
+ provide(addon, "targetApplications", [{id: TOOLKIT_ID,
+ minVersion: "42",
+ maxVersion: "*"}]);
+
+ file = createTempXPIFile(addon);
+ } else {
+ provide(addon, "manifest.version", version);
+ file = createTempWebExtensionFile(addon);
+ }
+ file.moveTo(addonsDir, `${id}-${version}.xpi`.replace(/[{}]/g, ""));
+
+ let path = `/addons/${file.leafName}`;
+ provide(update, "update_link", `http://localhost:${gPort}${path}`);
+
+ addonData.updates.push(update);
+ }
+
+ mapManifest(updatePath, { data: JSON.stringify(manifestJSON),
+ contentType: "application/json" });
+
+
+ let addon = yield awaitInstall;
+
+ let updates = yield promiseFindAddonUpdates(addon, aReason);
+ updates.addon = addon;
+
+ return updates;
+});
+
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "42.0", "42.0");
+
+ startupManager();
+ do_register_cleanup(promiseShutdownManager);
+
+ run_next_test();
+}
+
+
+// Check that compatibility updates are applied.
+add_task(function* checkUpdateMetadata() {
+ let update = yield checkUpdates({
+ addon: {
+ manifest: {
+ version: "1.0",
+ applications: { gecko: { strict_max_version: "45" } },
+ }
+ },
+ updates: {
+ "1.0": {
+ applications: { gecko: { strict_min_version: "40",
+ strict_max_version: "48" } },
+ }
+ }
+ });
+
+ ok(update.compatibilityUpdate, "have compat update");
+ ok(!update.updateAvailable, "have no add-on update");
+
+ ok(update.addon.isCompatibleWith("40", "40"), "compatible min");
+ ok(update.addon.isCompatibleWith("48", "48"), "compatible max");
+ ok(!update.addon.isCompatibleWith("49", "49"), "not compatible max");
+
+ update.addon.uninstall();
+});
+
+
+// Check that updates from web extensions to web extensions succeed.
+add_task(function* checkUpdateToWebExt() {
+ let update = yield checkUpdates({
+ addon: { manifest: { version: "1.0" } },
+ updates: {
+ "1.1": { },
+ "1.2": { },
+ "1.3": { "applications": { "gecko": { "strict_min_version": "48" } } },
+ }
+ });
+
+ ok(!update.compatibilityUpdate, "have no compat update");
+ ok(update.updateAvailable, "have add-on update");
+
+ equal(update.addon.version, "1.0", "add-on version");
+
+ yield promiseCompleteAllInstalls([update.updateAvailable]);
+
+ let addon = yield promiseAddonByID(update.addon.id);
+ equal(addon.version, "1.2", "new add-on version");
+
+ addon.uninstall();
+});
+
+
+// Check that updates from web extensions to XUL extensions fail.
+add_task(function* checkUpdateToRDF() {
+ let update = yield checkUpdates({
+ addon: { manifest: { version: "1.0" } },
+ updates: {
+ "1.1": { addon: { rdf: true } },
+ }
+ });
+
+ ok(!update.compatibilityUpdate, "have no compat update");
+ ok(update.updateAvailable, "have add-on update");
+
+ equal(update.addon.version, "1.0", "add-on version");
+
+ let result = yield new Promise((resolve, reject) => {
+ update.updateAvailable.addListener({
+ onDownloadFailed: resolve,
+ onDownloadEnded: reject,
+ onInstalling: reject,
+ onInstallStarted: reject,
+ onInstallEnded: reject,
+ });
+ update.updateAvailable.install();
+ });
+
+ equal(result.error, AddonManager.ERROR_UNEXPECTED_ADDON_TYPE, "error: unexpected add-on type");
+
+ let addon = yield promiseAddonByID(update.addon.id);
+ equal(addon.version, "1.0", "new add-on version");
+
+ addon.uninstall();
+});
+
+
+// Check that illegal update URLs are rejected.
+add_task(function* checkIllegalUpdateURL() {
+ const URLS = ["chrome://browser/content/",
+ "data:text/json,...",
+ "javascript:;",
+ "/"];
+
+ for (let url of URLS) {
+ let { messages } = yield promiseConsoleOutput(() => {
+ return new Promise((resolve, reject) => {
+ let addonFile = createTempWebExtensionFile({
+ manifest: { applications: { gecko: { update_url: url } } },
+ });
+
+ AddonManager.getInstallForFile(addonFile, install => {
+ Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
+
+ if (install && install.state == AddonManager.STATE_DOWNLOAD_FAILED)
+ resolve();
+ reject(new Error("Unexpected state: " + (install && install.state)))
+ });
+ });
+ });
+
+ ok(messages.some(msg => /Access denied for URL|may not load or link to|is not a valid URL/.test(msg)),
+ "Got checkLoadURI error");
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js
new file mode 100644
index 000000000..e8529bb2c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_updatecheck.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that AddonUpdateChecker works correctly
+
+Components.utils.import("resource://gre/modules/addons/AddonUpdateChecker.jsm");
+
+Components.utils.import("resource://testing-common/httpd.js");
+
+var testserver = createHttpServer(4444);
+testserver.registerDirectory("/data/", do_get_file("data"));
+
+function checkUpdates(aId, aUpdateKey, aUpdateFile) {
+ return new Promise((resolve, reject) => {
+ AddonUpdateChecker.checkForUpdates(aId, aUpdateKey, `http://localhost:4444/data/${aUpdateFile}`, {
+ onUpdateCheckComplete: resolve,
+
+ onUpdateCheckError: function(status) {
+ let error = new Error("Update check failed with status " + status);
+ error.status = status;
+ reject(error);
+ }
+ });
+ });
+}
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1");
+
+ run_next_test();
+}
+
+// Test that a basic update check returns the expected available updates
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ let updates = yield checkUpdates("updatecheck1@tests.mozilla.org", null, file);
+
+ equal(updates.length, 5);
+ let update = AddonUpdateChecker.getNewestCompatibleUpdate(updates);
+ notEqual(update, null);
+ equal(update.version, "3.0");
+ update = AddonUpdateChecker.getCompatibilityUpdate(updates, "2");
+ notEqual(update, null);
+ equal(update.version, "2.0");
+ equal(update.targetApplications[0].minVersion, "1");
+ equal(update.targetApplications[0].maxVersion, "2");
+ }
+});
+
+/*
+ * Tests that the security checks are applied correctly
+ *
+ * Test signature updateHash updateLink expected
+ *--------------------------------------------------------
+ * 2 absent absent http fail
+ * 3 broken absent http fail
+ * 4 correct absent http no update
+ * 5 correct sha1 http update
+ * 6 corrent absent https update
+ * 7 corrent sha1 https update
+ * 8 corrent md2 http no update
+ * 9 corrent md2 https update
+ */
+
+var updateKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDK426erD/H3XtsjvaB5+PJqbhj" +
+ "Zc9EDI5OCJS8R3FIObJ9ZHJK1TXeaE7JWqt9WUmBWTEFvwS+FI9vWu8058N9CHhD" +
+ "NyeP6i4LuUYjTURnn7Yw/IgzyIJ2oKsYa32RuxAyteqAWqPT/J63wBixIeCxmysf" +
+ "awB/zH4KaPiY3vnrzQIDAQAB";
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ try {
+ yield checkUpdates("test_bug378216_5@tests.mozilla.org",
+ updateKey, file);
+ throw "Expected the update check to fail";
+ } catch (e) {}
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ try {
+ yield checkUpdates("test_bug378216_7@tests.mozilla.org",
+ updateKey, file);
+
+ throw "Expected the update check to fail";
+ } catch (e) {}
+ }
+});
+
+add_task(function* () {
+ // Make sure that the JSON manifest is rejected when an update key is
+ // required, but perform the remaining tests which aren't expected to fail
+ // because of the update key, without requiring one for the JSON variant.
+
+ try {
+ let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org",
+ updateKey, "test_updatecheck.json");
+
+ throw "Expected the update check to fail";
+ } catch (e) {}
+
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_8@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ ok(!("updateURL" in updates[0]));
+ }
+});
+
+add_task(function* () {
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_9@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ equal(updates[0].version, "2.0");
+ ok("updateURL" in updates[0]);
+ }
+});
+
+add_task(function* () {
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_10@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ equal(updates[0].version, "2.0");
+ ok("updateURL" in updates[0]);
+ }
+});
+
+add_task(function* () {
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_11@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ equal(updates[0].version, "2.0");
+ ok("updateURL" in updates[0]);
+ }
+});
+
+add_task(function* () {
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_12@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ do_check_false("updateURL" in updates[0]);
+ }
+});
+
+add_task(function* () {
+ for (let [file, key] of [["test_updatecheck.rdf", updateKey],
+ ["test_updatecheck.json", null]]) {
+ let updates = yield checkUpdates("test_bug378216_13@tests.mozilla.org",
+ key, file);
+ equal(updates.length, 1);
+ equal(updates[0].version, "2.0");
+ ok("updateURL" in updates[0]);
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ let updates = yield checkUpdates("test_bug378216_14@tests.mozilla.org",
+ null, file);
+ equal(updates.length, 0);
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ try {
+ yield checkUpdates("test_bug378216_15@tests.mozilla.org",
+ null, file);
+
+ throw "Update check should have failed";
+ } catch (e) {
+ equal(e.status, AddonUpdateChecker.ERROR_PARSE_ERROR);
+ }
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ let updates = yield checkUpdates("ignore-compat@tests.mozilla.org",
+ null, file);
+ equal(updates.length, 3);
+ let update = AddonUpdateChecker.getNewestCompatibleUpdate(
+ updates, null, null, true);
+ notEqual(update, null);
+ equal(update.version, 2);
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ let updates = yield checkUpdates("compat-override@tests.mozilla.org",
+ null, file);
+ equal(updates.length, 3);
+ let overrides = [{
+ type: "incompatible",
+ minVersion: 1,
+ maxVersion: 2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 0.1,
+ appMaxVersion: 0.2
+ }, {
+ type: "incompatible",
+ minVersion: 2,
+ maxVersion: 2,
+ appID: "xpcshell@tests.mozilla.org",
+ appMinVersion: 1,
+ appMaxVersion: 2
+ }];
+ let update = AddonUpdateChecker.getNewestCompatibleUpdate(
+ updates, null, null, true, false, overrides);
+ notEqual(update, null);
+ equal(update.version, 1);
+ }
+});
+
+add_task(function* () {
+ for (let file of ["test_updatecheck.rdf", "test_updatecheck.json"]) {
+ let updates = yield checkUpdates("compat-strict-optin@tests.mozilla.org",
+ null, file);
+ equal(updates.length, 1);
+ let update = AddonUpdateChecker.getNewestCompatibleUpdate(
+ updates, null, null, true, false);
+ equal(update, null);
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js
new file mode 100644
index 000000000..f7e3e21e5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_updateid.js
@@ -0,0 +1,86 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that updating an add-on to a new ID works
+
+// The test extension uses an insecure update url.
+Services.prefs.setBoolPref("extensions.checkUpdateSecurity", false);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+function promiseInstallUpdate(install) {
+ return new Promise((resolve, reject) => {
+ install.addListener({
+ onDownloadFailed: () => {
+ let err = new Error("download error");
+ err.code = install.error;
+ reject(err);
+ },
+ onInstallFailed: () => {
+ let err = new Error("install error");
+ err.code = install.error;
+ reject(err);
+ },
+ onInstallEnded: resolve,
+ });
+
+ install.install();
+ });
+}
+
+// Create and configure the HTTP server.
+let testserver = createHttpServer(4444);
+testserver.registerDirectory("/data/", do_get_file("data"));
+testserver.registerDirectory("/addons/", do_get_file("addons"));
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+ run_next_test();
+}
+
+// Verify that an update to an add-on with a new ID fails
+add_task(function* test_update_new_id() {
+ yield promiseInstallFile(do_get_addon("test_updateid1"));
+
+ let addon = yield promiseAddonByID("addon1@tests.mozilla.org");
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+
+ let update = yield promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ let install = update.updateAvailable;
+ do_check_eq(install.name, addon.name);
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.existingAddon, addon);
+
+ yield Assert.rejects(promiseInstallUpdate(install),
+ function(err) { return err.code == AddonManager.ERROR_INCORRECT_ID },
+ "Upgrade to a different ID fails");
+
+ addon.uninstall();
+});
+
+// Verify that an update to a multi-package xpi fails
+add_task(function* test_update_new_id() {
+ yield promiseInstallFile(do_get_addon("test_update_multi1"));
+
+ let addon = yield promiseAddonByID("updatemulti@tests.mozilla.org");
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+
+ let update = yield promiseFindAddonUpdates(addon, AddonManager.UPDATE_WHEN_USER_REQUESTED);
+ let install = update.updateAvailable;
+ do_check_eq(install.name, addon.name);
+ do_check_eq(install.version, "2.0");
+ do_check_eq(install.state, AddonManager.STATE_AVAILABLE);
+ do_check_eq(install.existingAddon, addon);
+
+ yield Assert.rejects(promiseInstallUpdate(install),
+ function(err) { return err.code == AddonManager.ERROR_UNEXPECTED_ADDON_TYPE },
+ "Upgrade to a multipackage xpi fails");
+
+ addon.uninstall();
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
new file mode 100644
index 000000000..dc3d9438a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade.js
@@ -0,0 +1,206 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that app upgrades produce the expected behaviours,
+// with strict compatibility checking disabled.
+
+Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, false);
+
+// Enable loading extensions from the application scope
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE +
+ AddonManager.SCOPE_APPLICATION);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+const globalDir = Services.dirsvc.get("XREAddonAppDir", AM_Ci.nsIFile);
+globalDir.append("extensions");
+
+var gGlobalExisted = globalDir.exists();
+var gInstallTime = Date.now();
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Will be compatible in the first version and incompatible in subsequent versions
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ targetPlatforms: [
+ "XPCShell",
+ "WINNT_x86",
+ ]
+ }, profileDir);
+
+ // Works in all tested versions
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 2",
+ targetPlatforms: [
+ "XPCShell_noarch-spidermonkey"
+ ]
+ }, profileDir);
+
+ // Will be disabled in the first version and enabled in the second.
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ // Will be compatible in both versions but will change version in between
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ do_test_pending();
+
+ run_test_1();
+}
+
+function end_test() {
+ if (!gGlobalExisted) {
+ globalDir.remove(true);
+ }
+ else {
+ globalDir.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
+ globalDir.remove(true);
+ }
+ do_execute_soon(do_test_finished);
+}
+
+// Test that the test extensions are all installed
+function run_test_1() {
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "1.0");
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Test that upgrading the application doesn't disable now incompatible add-ons
+function run_test_2() {
+ // Upgrade the extension
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ restartManager("2");
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "2.0");
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Test that nothing changes when only the build ID changes.
+function run_test_3() {
+ // Upgrade the extension
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "3.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "3",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ // Simulates a simple Build ID change, the platform deletes extensions.ini
+ // whenever the application is changed.
+ gExtensionsINI.remove(true);
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "2.0");
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js
new file mode 100644
index 000000000..c2203d097
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_upgrade_strictcompat.js
@@ -0,0 +1,209 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This verifies that app upgrades produce the expected behaviours,
+// with strict compatibility checking enabled.
+
+// Enable loading extensions from the application scope
+Services.prefs.setIntPref("extensions.enabledScopes",
+ AddonManager.SCOPE_PROFILE +
+ AddonManager.SCOPE_APPLICATION);
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+const globalDir = Services.dirsvc.get("XREAddonAppDir", AM_Ci.nsIFile);
+globalDir.append("extensions");
+
+var gGlobalExisted = globalDir.exists();
+var gInstallTime = Date.now();
+
+function run_test() {
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+
+ // Will be enabled in the first version and disabled in subsequent versions
+ writeInstallRDFForExtension({
+ id: "addon1@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 1",
+ targetPlatforms: [
+ "XPCShell",
+ "WINNT_x86",
+ ]
+ }, profileDir);
+
+ // Works in all tested versions
+ writeInstallRDFForExtension({
+ id: "addon2@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 2",
+ targetPlatforms: [
+ "XPCShell_noarch-spidermonkey"
+ ]
+ }, profileDir);
+
+ // Will be disabled in the first version and enabled in the second.
+ writeInstallRDFForExtension({
+ id: "addon3@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 3",
+ }, profileDir);
+
+ // Will be enabled in both versions but will change version in between
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "1.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ do_test_pending();
+
+ Services.prefs.setBoolPref(PREF_EM_STRICT_COMPATIBILITY, true);
+
+ run_test_1();
+}
+
+function end_test() {
+ if (!gGlobalExisted) {
+ globalDir.remove(true);
+ }
+ else {
+ globalDir.append(do_get_expected_addon_name("addon4@tests.mozilla.org"));
+ globalDir.remove(true);
+ }
+
+ Services.prefs.clearUserPref(PREF_EM_STRICT_COMPATIBILITY);
+
+ do_execute_soon(do_test_finished);
+}
+
+// Test that the test extensions are all installed
+function run_test_1() {
+ startupManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_false(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "1.0");
+
+ do_execute_soon(run_test_2);
+ });
+}
+
+// Test that upgrading the application disables now incompatible add-ons
+function run_test_2() {
+ // Upgrade the extension
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "2.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "2",
+ maxVersion: "2"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ restartManager("2");
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "2.0");
+
+ do_execute_soon(run_test_3);
+ });
+}
+
+// Test that nothing changes when only the build ID changes.
+function run_test_3() {
+ // Upgrade the extension
+ var dest = writeInstallRDFForExtension({
+ id: "addon4@tests.mozilla.org",
+ version: "3.0",
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "3",
+ maxVersion: "3"
+ }],
+ name: "Test Addon 4",
+ }, globalDir);
+ setExtensionModifiedTime(dest, gInstallTime);
+
+ // Simulates a simple Build ID change, the platform deletes extensions.ini
+ // whenever the application is changed.
+ gExtensionsINI.remove(true);
+ restartManager();
+
+ AddonManager.getAddonsByIDs(["addon1@tests.mozilla.org",
+ "addon2@tests.mozilla.org",
+ "addon3@tests.mozilla.org",
+ "addon4@tests.mozilla.org"],
+ function([a1, a2, a3, a4]) {
+
+ do_check_neq(a1, null);
+ do_check_false(isExtensionInAddonsList(profileDir, a1.id));
+
+ do_check_neq(a2, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a2.id));
+
+ do_check_neq(a3, null);
+ do_check_true(isExtensionInAddonsList(profileDir, a3.id));
+
+ do_check_neq(a4, null);
+ do_check_true(isExtensionInAddonsList(globalDir, a4.id));
+ do_check_eq(a4.version, "2.0");
+
+ end_test();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
new file mode 100644
index 000000000..b51f47977
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -0,0 +1,421 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
+const ID = "webextension1@tests.mozilla.org";
+
+const PREF_SELECTED_LOCALE = "general.useragent.locale";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+const { GlobalManager, Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseAddonStartup() {
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("ready", listener);
+ resolve(extension);
+ };
+
+ Management.on("ready", listener);
+ });
+}
+
+function promiseInstallWebExtension(aData) {
+ let addonFile = createTempWebExtensionFile(aData);
+
+ return promiseInstallAllFiles([addonFile]).then(() => {
+ Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
+ return promiseAddonStartup();
+ });
+}
+
+add_task(function*() {
+ equal(GlobalManager.extensionMap.size, 0);
+
+ yield Promise.all([
+ promiseInstallAllFiles([do_get_addon("webextension_1")], true),
+ promiseAddonStartup()
+ ]);
+
+ equal(GlobalManager.extensionMap.size, 1);
+ ok(GlobalManager.extensionMap.has(ID));
+
+ let chromeReg = AM_Cc["@mozilla.org/chrome/chrome-registry;1"].
+ getService(AM_Ci.nsIChromeRegistry);
+ try {
+ chromeReg.convertChromeURL(NetUtil.newURI("chrome://webex/content/webex.xul"));
+ do_throw("Chrome manifest should not have been registered");
+ }
+ catch (e) {
+ // Expected the chrome url to not be registered
+ }
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Web Extension Name");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_false(addon.isSystem);
+ do_check_eq(addon.type, "extension");
+ do_check_true(addon.isWebExtension);
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let uri = do_get_addon_root_uri(profileDir, ID);
+
+ do_check_eq(addon.iconURL, uri + "icon48.png");
+ do_check_eq(addon.icon64URL, uri + "icon64.png");
+
+ // Should persist through a restart
+ yield promiseShutdownManager();
+
+ equal(GlobalManager.extensionMap.size, 0);
+
+ startupManager();
+ yield promiseAddonStartup();
+
+ equal(GlobalManager.extensionMap.size, 1);
+ ok(GlobalManager.extensionMap.has(ID));
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Web Extension Name");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_false(addon.isSystem);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let file = getFileForAddon(profileDir, ID);
+ do_check_true(file.exists());
+
+ uri = do_get_addon_root_uri(profileDir, ID);
+
+ do_check_eq(addon.iconURL, uri + "icon48.png");
+ do_check_eq(addon.icon64URL, uri + "icon64.png");
+
+ addon.userDisabled = true;
+
+ equal(GlobalManager.extensionMap.size, 0);
+
+ addon.userDisabled = false;
+ yield promiseAddonStartup();
+
+ equal(GlobalManager.extensionMap.size, 1);
+ ok(GlobalManager.extensionMap.has(ID));
+
+ addon.uninstall();
+
+ equal(GlobalManager.extensionMap.size, 0);
+ do_check_false(GlobalManager.extensionMap.has(ID));
+
+ yield promiseShutdownManager();
+});
+
+// Writing the manifest direct to the profile should work
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ version: "1.0",
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }, profileDir);
+
+ startupManager();
+ yield promiseAddonStartup();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+ do_check_eq(addon.version, "1.0");
+ do_check_eq(addon.name, "Web Extension Name");
+ do_check_true(addon.isCompatible);
+ do_check_false(addon.appDisabled);
+ do_check_true(addon.isActive);
+ do_check_false(addon.isSystem);
+ do_check_eq(addon.type, "extension");
+ do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_SIGNED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+ let file = getFileForAddon(profileDir, ID);
+ do_check_true(file.exists());
+
+ addon.uninstall();
+
+ yield promiseRestartManager();
+});
+
+add_task(function* test_manifest_localization() {
+ const extensionId = "webextension3@tests.mozilla.org";
+
+ yield promiseInstallAllFiles([do_get_addon("webextension_3")], true);
+ yield promiseAddonStartup();
+
+ let addon = yield promiseAddonByID(extensionId);
+ addon.userDisabled = true;
+
+ equal(addon.name, "Web Extensiøn foo ☹");
+ equal(addon.description, "Descriptïon bar ☹ of add-on");
+
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "fr-FR");
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(extensionId);
+
+ equal(addon.name, "Web Extensiøn le foo ☺");
+ equal(addon.description, "Descriptïon le bar ☺ of add-on");
+
+ Services.prefs.setCharPref(PREF_SELECTED_LOCALE, "de");
+ yield promiseRestartManager();
+
+ addon = yield promiseAddonByID(extensionId);
+
+ equal(addon.name, "Web Extensiøn foo ☹");
+ equal(addon.description, "Descriptïon bar ☹ of add-on");
+
+ addon.uninstall();
+});
+
+// Missing version should cause a failure
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }, profileDir);
+
+ yield promiseRestartManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ let file = getFileForAddon(profileDir, ID);
+ do_check_false(file.exists());
+
+ yield promiseRestartManager();
+});
+
+// Incorrect manifest version should cause a failure
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ version: "1.0",
+ manifest_version: 1,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }, profileDir);
+
+ yield promiseRestartManager();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_eq(addon, null);
+
+ let file = getFileForAddon(profileDir, ID);
+ do_check_false(file.exists());
+
+ yield promiseRestartManager();
+});
+
+// install.rdf should be read before manifest.json
+add_task(function*() {
+
+ yield Promise.all([
+ promiseInstallAllFiles([do_get_addon("webextension_2")], true)
+ ]);
+
+ yield promiseRestartManager();
+
+ let installrdf_id = "first-webextension2@tests.mozilla.org";
+ let first_addon = yield promiseAddonByID(installrdf_id);
+ do_check_neq(first_addon, null);
+ do_check_false(first_addon.appDisabled);
+ do_check_true(first_addon.isActive);
+ do_check_false(first_addon.isSystem);
+
+ let manifestjson_id= "last-webextension2@tests.mozilla.org";
+ let last_addon = yield promiseAddonByID(manifestjson_id);
+ do_check_eq(last_addon, null);
+
+ yield promiseRestartManager();
+});
+
+// Test that the "options_ui" manifest section is processed correctly.
+add_task(function* test_options_ui() {
+ let OPTIONS_RE = /^moz-extension:\/\/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\/options\.html$/;
+
+ const extensionId = "webextension@tests.mozilla.org";
+ yield promiseInstallWebExtension({
+ manifest: {
+ applications: {gecko: {id: extensionId}},
+ "options_ui": {
+ "page": "options.html",
+ },
+ },
+ });
+
+ let addon = yield promiseAddonByID(extensionId);
+ equal(addon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
+ "Addon should have an INLINE_BROWSER options type");
+
+ ok(OPTIONS_RE.test(addon.optionsURL),
+ "Addon should have a moz-extension: options URL for /options.html");
+
+ addon.uninstall();
+
+ const ID2 = "webextension2@tests.mozilla.org";
+ yield promiseInstallWebExtension({
+ manifest: {
+ applications: {gecko: {id: ID2}},
+ "options_ui": {
+ "page": "options.html",
+ "open_in_tab": true,
+ },
+ },
+ });
+
+ addon = yield promiseAddonByID(ID2);
+ equal(addon.optionsType, AddonManager.OPTIONS_TYPE_TAB,
+ "Addon should have a TAB options type");
+
+ ok(OPTIONS_RE.test(addon.optionsURL),
+ "Addon should have a moz-extension: options URL for /options.html");
+
+ addon.uninstall();
+});
+
+// Test that experiments permissions add the appropriate dependencies.
+add_task(function* test_experiments_dependencies() {
+ if (AppConstants.RELEASE_OR_BETA)
+ // Experiments are not enabled on release builds.
+ return;
+
+ let addonFile = createTempWebExtensionFile({
+ manifest: {
+ applications: {gecko: {id: "meh@experiment"}},
+ "permissions": ["experiments.meh"],
+ },
+ });
+
+ yield promiseInstallAllFiles([addonFile]);
+
+ let addon = yield new Promise(resolve => AddonManager.getAddonByID("meh@experiment", resolve));
+
+ deepEqual(addon.dependencies, ["meh@experiments.addons.mozilla.org"],
+ "Addon should have the expected dependencies");
+
+ equal(addon.appDisabled, true, "Add-on should be app disabled due to missing dependencies");
+
+ addon.uninstall();
+});
+
+// Test that experiments API extensions install correctly.
+add_task(function* test_experiments_api() {
+ if (AppConstants.RELEASE_OR_BETA)
+ // Experiments are not enabled on release builds.
+ return;
+
+ const extensionId = "meh@experiments.addons.mozilla.org";
+
+ let addonFile = createTempXPIFile({
+ id: extensionId,
+ type: 256,
+ version: "0.1",
+ name: "Meh API",
+ });
+
+ yield promiseInstallAllFiles([addonFile]);
+
+ let addons = yield new Promise(resolve => AddonManager.getAddonsByTypes(["apiextension"], resolve));
+ let addon = addons.pop();
+ equal(addon.id, extensionId, "Add-on should be installed as an API extension");
+
+ addons = yield new Promise(resolve => AddonManager.getAddonsByTypes(["extension"], resolve));
+ equal(addons.pop().id, extensionId, "Add-on type should be aliased to extension");
+
+ addon.uninstall();
+});
+
+add_task(function* developerShouldOverride() {
+ let addon = yield promiseInstallWebExtension({
+ manifest: {
+ default_locale: "en",
+ developer: {
+ name: "__MSG_name__",
+ url: "__MSG_url__"
+ },
+ author: "Will be overridden by developer",
+ homepage_url: "https://will.be.overridden",
+ },
+ files: {
+ "_locales/en/messages.json": `{
+ "name": {
+ "message": "en name"
+ },
+ "url": {
+ "message": "https://example.net/en"
+ }
+ }`
+ }
+ });
+
+ addon = yield promiseAddonByID(addon.id);
+ equal(addon.creator, "en name");
+ equal(addon.homepageURL, "https://example.net/en");
+ addon.uninstall();
+});
+
+add_task(function* developerEmpty() {
+ for (let developer of [{}, null, {name: null, url: null}]) {
+ let addon = yield promiseInstallWebExtension({
+ manifest: {
+ author: "Some author",
+ developer: developer,
+ homepage_url: "https://example.net",
+ manifest_version: 2,
+ name: "Web Extension Name",
+ version: "1.0",
+ }
+ });
+
+ addon = yield promiseAddonByID(addon.id);
+ equal(addon.creator, "Some author");
+ equal(addon.homepageURL, "https://example.net");
+ addon.uninstall();
+ }
+});
+
+add_task(function* authorNotString() {
+ for (let author of [{}, [], 42]) {
+ let addon = yield promiseInstallWebExtension({
+ manifest: {
+ author: author,
+ manifest_version: 2,
+ name: "Web Extension Name",
+ version: "1.0",
+ }
+ });
+
+ addon = yield promiseAddonByID(addon.id);
+ equal(addon.creator, null);
+ addon.uninstall();
+ }
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
new file mode 100644
index 000000000..3bd8a2bd8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_embedded.js
@@ -0,0 +1,306 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+BootstrapMonitor.init();
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "49");
+startupManager();
+
+// NOTE: the following import needs to be called after the `createAppInfo`
+// or it will fail Extension.jsm internally imports AddonManager.jsm and
+// AddonManager will raise a ReferenceError exception because it tried to
+// access an undefined `Services.appinfo` object.
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+const {
+ EmbeddedExtensionManager,
+ LegacyExtensionsUtils,
+} = Components.utils.import("resource://gre/modules/LegacyExtensionsUtils.jsm");
+
+// Wait the startup of the embedded webextension.
+function promiseWebExtensionStartup() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+function promiseWebExtensionShutdown() {
+ return new Promise(resolve => {
+ let listener = (event, extension) => {
+ Management.off("shutdown", listener);
+ resolve(extension);
+ };
+
+ Management.on("shutdown", listener);
+ });
+}
+
+const BOOTSTRAP = String.raw`
+ Components.utils.import("resource://xpcshell-data/BootstrapMonitor.jsm").monitor(this);
+`;
+
+const EMBEDDED_WEBEXT_MANIFEST = JSON.stringify({
+ name: "embedded webextension addon",
+ manifest_version: 2,
+ version: "1.0",
+});
+
+/**
+ * This test case checks that an hasEmbeddedWebExtension addon property
+ * is persisted and restored correctly across restarts.
+ */
+add_task(function* has_embedded_webextension_persisted() {
+ const ID = "embedded-webextension-addon-persist@tests.mozilla.org";
+
+ const xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ hasEmbeddedWebExtension: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ }, {
+ "bootstrap.js": BOOTSTRAP,
+ "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+ });
+
+ yield promiseInstallFile(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+ equal(addon.hasEmbeddedWebExtension, true,
+ "Got the expected hasEmbeddedWebExtension value");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let startupInfo = BootstrapMonitor.started.get(ID);
+ ok(("webExtension" in startupInfo.data),
+ "Got an webExtension property in the startup bootstrap method params");
+ ok(("startup" in startupInfo.data.webExtension),
+ "Got the expected 'startup' property in the webExtension object");
+
+ // After restarting the manager, the add-on should still have the
+ // hasEmbeddedWebExtension property as expected.
+ yield promiseRestartManager();
+
+ let persisted = JSON.parse(Services.prefs.getCharPref("extensions.bootstrappedAddons"));
+ ok(ID in persisted, "Hybrid add-on persisted to bootstrappedAddons.");
+ equal(persisted[ID].hasEmbeddedWebExtension, true,
+ "hasEmbeddedWebExtension flag persisted to bootstrappedAddons.");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.hasEmbeddedWebExtension, true, "Got the expected hasEmbeddedWebExtension value");
+
+ // Check that the addon has been installed and started.
+ let newStartupInfo = BootstrapMonitor.started.get(ID);
+ ok(("webExtension" in newStartupInfo.data),
+ "Got an webExtension property in the startup bootstrap method params");
+ ok(("startup" in newStartupInfo.data.webExtension),
+ "Got the expected 'startup' property in the webExtension object");
+
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstall;
+});
+
+/**
+ * This test case checks that an addon with hasEmbeddedWebExtension set to true
+ * in its install.rdf gets the expected `embeddedWebExtension` object in the
+ * parameters of its bootstrap methods.
+ */
+add_task(function* run_embedded_webext_bootstrap() {
+ const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+ const xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ hasEmbeddedWebExtension: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ }, {
+ "bootstrap.js": BOOTSTRAP,
+ "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+ });
+
+ yield AddonManager.installTemporaryAddon(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+ equal(addon.hasEmbeddedWebExtension, true,
+ "Got the expected hasEmbeddedWebExtension value");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+
+ let installInfo = BootstrapMonitor.installed.get(ID);
+ ok(!("webExtension" in installInfo.data),
+ "No webExtension property is expected in the install bootstrap method params");
+
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ let startupInfo = BootstrapMonitor.started.get(ID);
+
+ ok(("webExtension" in startupInfo.data),
+ "Got an webExtension property in the startup bootstrap method params");
+
+ ok(("startup" in startupInfo.data.webExtension),
+ "Got the expected 'startup' property in the webExtension object");
+
+ const waitForWebExtensionStartup = promiseWebExtensionStartup();
+
+ const embeddedAPI = yield startupInfo.data.webExtension.startup();
+
+ // WebExtension startup should have been fully resolved.
+ yield waitForWebExtensionStartup;
+
+ Assert.deepEqual(
+ Object.keys(embeddedAPI.browser.runtime).sort(),
+ ["onConnect", "onMessage"],
+ `Got the expected 'runtime' in the 'browser' API object`
+ );
+
+ // Uninstall the addon and wait that the embedded webextension has been stopped and
+ // test the params of the shutdown and uninstall bootstrap method.
+ let waitForWebExtensionShutdown = promiseWebExtensionShutdown();
+ let waitUninstall = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitForWebExtensionShutdown;
+ yield waitUninstall;
+
+ BootstrapMonitor.checkAddonNotStarted(ID, "1.0");
+
+ let shutdownInfo = BootstrapMonitor.stopped.get(ID);
+ ok(!("webExtension" in shutdownInfo.data),
+ "No webExtension property is expected in the shutdown bootstrap method params");
+
+ let uninstallInfo = BootstrapMonitor.uninstalled.get(ID);
+ ok(!("webExtension" in uninstallInfo.data),
+ "No webExtension property is expected in the uninstall bootstrap method params");
+});
+
+/**
+ * This test case checks that an addon with hasEmbeddedWebExtension can be reloaded
+ * without raising unexpected exceptions due to race conditions.
+ */
+add_task(function* reload_embedded_webext_bootstrap() {
+ const ID = "embedded-webextension-addon2@tests.mozilla.org";
+
+ // No embedded webextension should be currently around.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked here");
+
+ const xpiFile = createTempXPIFile({
+ id: ID,
+ name: "Test Add-on",
+ version: "1.0",
+ bootstrap: true,
+ hasEmbeddedWebExtension: true,
+ targetApplications: [{
+ id: "xpcshell@tests.mozilla.org",
+ minVersion: "1",
+ maxVersion: "1.9.2"
+ }]
+ }, {
+ "bootstrap.js": BOOTSTRAP,
+ "webextension/manifest.json": EMBEDDED_WEBEXT_MANIFEST,
+ });
+
+ yield AddonManager.installTemporaryAddon(xpiFile);
+
+ let addon = yield promiseAddonByID(ID);
+
+ notEqual(addon, null, "Got an addon object as expected");
+ equal(addon.version, "1.0", "Got the expected version");
+ equal(addon.isActive, true, "The Addon is active");
+ equal(addon.appDisabled, false, "The addon is not app disabled");
+ equal(addon.userDisabled, false, "The addon is not user disabled");
+
+ // Check that the addon has been installed and started.
+ BootstrapMonitor.checkAddonInstalled(ID, "1.0");
+ BootstrapMonitor.checkAddonStarted(ID, "1.0");
+
+ // Only one embedded extension.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtension = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+
+ let startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ const waitForAddonDisabled = promiseAddonEvent("onDisabled");
+ addon.userDisabled = true;
+ yield waitForAddonDisabled;
+
+ // No embedded webextension should be currently around.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked here");
+
+ const waitForAddonEnabled = promiseAddonEvent("onEnabled");
+ addon.userDisabled = false;
+ yield waitForAddonEnabled;
+
+ // Only one embedded extension.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtensionAfterEnabled = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+ notEqual(embeddedWebExtensionAfterEnabled, embeddedWebExtension,
+ "Got a new EmbeddedExtension instance after the addon has been disabled and then enabled");
+
+ startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ const waitForReinstalled = promiseAddonEvent("onInstalled");
+ addon.reload();
+ yield waitForReinstalled;
+
+ // No leaked embedded extension after the previous reloads.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 1,
+ "Got the expected number of tracked extension instances");
+
+ const embeddedWebExtensionAfterReload = EmbeddedExtensionManager.embeddedExtensionsByAddonId.get(ID);
+ notEqual(embeddedWebExtensionAfterReload, embeddedWebExtensionAfterEnabled,
+ "Got a new EmbeddedExtension instance after the addon has been reloaded");
+
+ startupInfo = BootstrapMonitor.started.get(ID);
+ yield startupInfo.data.webExtension.startup();
+
+ // Uninstall the test addon
+ let waitUninstalled = promiseAddonEvent("onUninstalled");
+ addon.uninstall();
+ yield waitUninstalled;
+
+ // No leaked embedded extension after uninstalling.
+ equal(EmbeddedExtensionManager.embeddedExtensionsByAddonId.size, 0,
+ "No embedded extension instance should be tracked after the addon uninstall");
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
new file mode 100644
index 000000000..25fb4115f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_icons.js
@@ -0,0 +1,169 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const ID = "webextension1@tests.mozilla.org";
+
+const profileDir = gProfD.clone();
+profileDir.append("extensions");
+profileDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
+startupManager();
+
+const { Management } = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+
+function promiseAddonStartup() {
+ return new Promise(resolve => {
+ let listener = (evt, extension) => {
+ Management.off("startup", listener);
+ resolve(extension);
+ };
+
+ Management.on("startup", listener);
+ });
+}
+
+// Test simple icon set parsing
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ version: "1.0",
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ },
+ icons: {
+ 16: "icon16.png",
+ 32: "icon32.png",
+ 48: "icon48.png",
+ 64: "icon64.png"
+ }
+ }, profileDir);
+
+ yield promiseRestartManager();
+ yield promiseAddonStartup();
+
+ let uri = do_get_addon_root_uri(profileDir, ID);
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ function check_icons(addon_copy) {
+ deepEqual(addon_copy.icons, {
+ 16: uri + "icon16.png",
+ 32: uri + "icon32.png",
+ 48: uri + "icon48.png",
+ 64: uri + "icon64.png"
+ });
+
+ // iconURL should map to icons[48] and icons[64]
+ equal(addon.iconURL, uri + "icon48.png");
+ equal(addon.icon64URL, uri + "icon64.png");
+
+ // AddonManager gets the correct icon sizes from addon.icons
+ equal(AddonManager.getPreferredIconURL(addon, 1), uri + "icon16.png");
+ equal(AddonManager.getPreferredIconURL(addon, 16), uri + "icon16.png");
+ equal(AddonManager.getPreferredIconURL(addon, 30), uri + "icon32.png");
+ equal(AddonManager.getPreferredIconURL(addon, 48), uri + "icon48.png");
+ equal(AddonManager.getPreferredIconURL(addon, 64), uri + "icon64.png");
+ equal(AddonManager.getPreferredIconURL(addon, 128), uri + "icon64.png");
+ }
+
+ check_icons(addon);
+
+ // check if icons are persisted through a restart
+ yield promiseRestartManager();
+ yield promiseAddonStartup();
+
+ addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ check_icons(addon);
+
+ addon.uninstall();
+
+ yield promiseRestartManager();
+});
+
+// Test AddonManager.getPreferredIconURL for retina screen sizes
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ version: "1.0",
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ },
+ icons: {
+ 32: "icon32.png",
+ 48: "icon48.png",
+ 64: "icon64.png",
+ 128: "icon128.png",
+ 256: "icon256.png"
+ }
+ }, profileDir);
+
+ yield promiseRestartManager();
+ yield promiseAddonStartup();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ let uri = do_get_addon_root_uri(profileDir, ID);
+
+ // AddonManager displays larger icons for higher pixel density
+ equal(AddonManager.getPreferredIconURL(addon, 32, {
+ devicePixelRatio: 2
+ }), uri + "icon64.png");
+
+ equal(AddonManager.getPreferredIconURL(addon, 48, {
+ devicePixelRatio: 2
+ }), uri + "icon128.png");
+
+ equal(AddonManager.getPreferredIconURL(addon, 64, {
+ devicePixelRatio: 2
+ }), uri + "icon128.png");
+
+ addon.uninstall();
+
+ yield promiseRestartManager();
+});
+
+// Handles no icons gracefully
+add_task(function*() {
+ yield promiseWriteWebManifestForExtension({
+ name: "Web Extension Name",
+ version: "1.0",
+ manifest_version: 2,
+ applications: {
+ gecko: {
+ id: ID
+ }
+ }
+ }, profileDir);
+
+ yield promiseRestartManager();
+ yield promiseAddonStartup();
+
+ let addon = yield promiseAddonByID(ID);
+ do_check_neq(addon, null);
+
+ let uri = do_get_addon_root_uri(profileDir, ID);
+
+ deepEqual(addon.icons, {});
+
+ equal(addon.iconURL, null);
+ equal(addon.icon64URL, null);
+
+ equal(AddonManager.getPreferredIconURL(addon, 128), null);
+
+ addon.uninstall();
+
+ yield promiseRestartManager();
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
new file mode 100644
index 000000000..1e7c9d9b7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_install.js
@@ -0,0 +1,478 @@
+
+const {ADDON_SIGNING} = AM_Cu.import("resource://gre/modules/addons/AddonConstants.jsm", {});
+
+function run_test() {
+ run_next_test();
+}
+
+let profileDir;
+add_task(function* setup() {
+ profileDir = gProfD.clone();
+ profileDir.append("extensions");
+
+ if (!profileDir.exists())
+ profileDir.create(AM_Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+});
+
+const IMPLICIT_ID_XPI = "data/webext-implicit-id.xpi";
+const IMPLICIT_ID_ID = "webext_implicit_id@tests.mozilla.org";
+
+// webext-implicit-id.xpi has a minimal manifest with no
+// applications or browser_specific_settings, so its id comes
+// from its signature, which should be the ID constant defined below.
+add_task(function* test_implicit_id() {
+ // This test needs to read the xpi certificate which only works
+ // if signing is enabled.
+ ok(ADDON_SIGNING, "Add-on signing is enabled");
+
+ let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+ equal(addon, null, "Add-on is not installed");
+
+ let xpifile = do_get_file(IMPLICIT_ID_XPI);
+ yield promiseInstallAllFiles([xpifile]);
+
+ addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+ notEqual(addon, null, "Add-on is installed");
+
+ addon.uninstall();
+});
+
+// We should also be able to install webext-implicit-id.xpi temporarily
+// and it should look just like the regular install (ie, the ID should
+// come from the signature)
+add_task(function* test_implicit_id_temp() {
+ // This test needs to read the xpi certificate which only works
+ // if signing is enabled.
+ ok(ADDON_SIGNING, "Add-on signing is enabled");
+
+ let addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+ equal(addon, null, "Add-on is not installed");
+
+ let xpifile = do_get_file(IMPLICIT_ID_XPI);
+ yield AddonManager.installTemporaryAddon(xpifile);
+
+ addon = yield promiseAddonByID(IMPLICIT_ID_ID);
+ notEqual(addon, null, "Add-on is installed");
+
+ // The sourceURI of a temporary installed addon should be equal to the
+ // file url of the installed xpi file.
+ equal(addon.sourceURI && addon.sourceURI.spec,
+ Services.io.newFileURI(xpifile).spec,
+ "SourceURI of the add-on has the expected value");
+
+ addon.uninstall();
+});
+
+// We should be able to temporarily install an unsigned web extension
+// that does not have an ID in its manifest.
+add_task(function* test_unsigned_no_id_temp_install() {
+ AddonTestUtils.useRealCertChecks = true;
+ const manifest = {
+ name: "no ID",
+ description: "extension without an ID",
+ manifest_version: 2,
+ version: "1.0"
+ };
+
+ const addonDir = yield promiseWriteWebManifestForExtension(manifest, gTmpD,
+ "the-addon-sub-dir");
+ const addon = yield AddonManager.installTemporaryAddon(addonDir);
+ ok(addon.id, "ID should have been auto-generated");
+
+ // The sourceURI of a temporary installed addon should be equal to the
+ // file url of the installed source dir.
+ equal(addon.sourceURI && addon.sourceURI.spec,
+ Services.io.newFileURI(addonDir).spec,
+ "SourceURI of the add-on has the expected value");
+
+ // Install the same directory again, as if re-installing or reloading.
+ const secondAddon = yield AddonManager.installTemporaryAddon(addonDir);
+ // The IDs should be the same.
+ equal(secondAddon.id, addon.id, "Reinstalled add-on has the expected ID");
+
+ secondAddon.uninstall();
+ Services.obs.notifyObservers(addonDir, "flush-cache-entry", null);
+ addonDir.remove(true);
+ AddonTestUtils.useRealCertChecks = false;
+});
+
+// We should be able to install two extensions from manifests without IDs
+// at different locations and get two unique extensions.
+add_task(function* test_multiple_no_id_extensions() {
+ AddonTestUtils.useRealCertChecks = true;
+ const manifest = {
+ name: "no ID",
+ description: "extension without an ID",
+ manifest_version: 2,
+ version: "1.0"
+ };
+
+ let extension1 = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+ useAddonManager: "temporary",
+ });
+
+ let extension2 = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+ useAddonManager: "temporary",
+ });
+
+ yield Promise.all([extension1.startup(), extension2.startup()]);
+
+ const allAddons = yield new Promise(resolve => {
+ AddonManager.getAllAddons(addons => resolve(addons));
+ });
+ do_print(`Found these add-ons: ${allAddons.map(a => a.name).join(", ")}`);
+ const filtered = allAddons.filter(addon => addon.name === manifest.name);
+ // Make sure we have two add-ons by the same name.
+ equal(filtered.length, 2, "Two add-ons are installed with the same name");
+
+ yield extension1.unload();
+ yield extension2.unload();
+ AddonTestUtils.useRealCertChecks = false;
+});
+
+// Test that we can get the ID from browser_specific_settings
+add_task(function* test_bss_id() {
+ const ID = "webext_bss_id@tests.mozilla.org";
+
+ let manifest = {
+ name: "bss test",
+ description: "test that ID may be in browser_specific_settings",
+ manifest_version: 2,
+ version: "1.0",
+
+ browser_specific_settings: {
+ gecko: {
+ id: ID
+ }
+ }
+ };
+
+ let addon = yield promiseAddonByID(ID);
+ equal(addon, null, "Add-on is not installed");
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+ useAddonManager: "temporary",
+ });
+ yield extension.startup();
+
+ addon = yield promiseAddonByID(ID);
+ notEqual(addon, null, "Add-on is installed");
+
+ yield extension.unload();
+});
+
+// Test that if we have IDs in both browser_specific_settings and applications,
+// that we prefer the ID in browser_specific_settings.
+add_task(function* test_two_ids() {
+ const GOOD_ID = "two_ids@tests.mozilla.org";
+ const BAD_ID = "i_am_obsolete@tests.mozilla.org";
+
+ let manifest = {
+ name: "two id test",
+ description: "test a web extension with ids in both applications and browser_specific_settings",
+ manifest_version: 2,
+ version: "1.0",
+
+ applications: {
+ gecko: {
+ id: BAD_ID
+ }
+ },
+
+ browser_specific_settings: {
+ gecko: {
+ id: GOOD_ID
+ }
+ }
+ }
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: manifest,
+ useAddonManager: "temporary",
+ });
+ yield extension.startup();
+
+ let addon = yield promiseAddonByID(BAD_ID);
+ equal(addon, null, "Add-on is not found using bad ID");
+ addon = yield promiseAddonByID(GOOD_ID);
+ notEqual(addon, null, "Add-on is found using good ID");
+
+ yield extension.unload();
+});
+
+// Test that strict_min_version and strict_max_version are enforced for
+// loading temporary extension.
+add_task(function* test_strict_min_max() {
+ // the app version being compared to is 1.9.2
+ const addonId = "strict_min_max@tests.mozilla.org";
+ const MANIFEST = {
+ name: "strict min max test",
+ description: "test strict min and max with temporary loading",
+ manifest_version: 2,
+ version: "1.0",
+ };
+
+ // bad max good min
+ let apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_min_version: "1",
+ strict_max_version: "1"
+ },
+ },
+ }
+ let testManifest = Object.assign(apps, MANIFEST);
+
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ let expectedMsg = new RegExp("Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " +
+ "add-on minVersion: 1. add-on maxVersion: 1.");
+ yield Assert.rejects(extension.startup(),
+ expectedMsg,
+ "Install rejects when specified maxVersion is not valid");
+
+ let addon = yield promiseAddonByID(addonId);
+ equal(addon, null, "Add-on is not installed");
+
+ // bad min good max
+ apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_min_version: "2",
+ strict_max_version: "2"
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ expectedMsg = new RegExp("Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " +
+ "add-on minVersion: 2. add-on maxVersion: 2.");
+ yield Assert.rejects(extension.startup(),
+ expectedMsg,
+ "Install rejects when specified minVersion is not valid");
+
+ addon = yield promiseAddonByID(addonId);
+ equal(addon, null, "Add-on is not installed");
+
+ // bad both
+ apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_min_version: "2",
+ strict_max_version: "1"
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ expectedMsg = new RegExp("Add-on strict_min_max@tests.mozilla.org is not compatible with application version. " +
+ "add-on minVersion: 2. add-on maxVersion: 1.");
+ yield Assert.rejects(extension.startup(),
+ expectedMsg,
+ "Install rejects when specified minVersion and maxVersion are not valid");
+
+ addon = yield promiseAddonByID(addonId);
+ equal(addon, null, "Add-on is not installed");
+
+ // bad only min
+ apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_min_version: "2"
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ expectedMsg = new RegExp("Add-on strict_min_max@tests.mozilla.org is not compatible with application version\. " +
+ "add-on minVersion: 2\.");
+ yield Assert.rejects(extension.startup(),
+ expectedMsg,
+ "Install rejects when specified minVersion and maxVersion are not valid");
+
+ addon = yield promiseAddonByID(addonId);
+ equal(addon, null, "Add-on is not installed");
+
+ // bad only max
+ apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_max_version: "1"
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ expectedMsg = new RegExp("Add-on strict_min_max@tests.mozilla.org is not compatible with application version\. " +
+ "add-on maxVersion: 1\.");
+ yield Assert.rejects(extension.startup(),
+ expectedMsg,
+ "Install rejects when specified minVersion and maxVersion are not valid");
+
+ addon = yield promiseAddonByID(addonId);
+ equal(addon, null, "Add-on is not installed");
+
+ // good both
+ apps = {
+ applications: {
+ gecko: {
+ id: addonId,
+ strict_min_version: "1",
+ strict_max_version: "2"
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ addon = yield promiseAddonByID(addonId);
+
+ notEqual(addon, null, "Add-on is installed");
+ equal(addon.id, addonId, "Installed add-on has the expected ID");
+ yield extension.unload();
+
+ // good only min
+ let newId = "strict_min_only@tests.mozilla.org";
+ apps = {
+ applications: {
+ gecko: {
+ id: newId,
+ strict_min_version: "1",
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ addon = yield promiseAddonByID(newId);
+
+ notEqual(addon, null, "Add-on is installed");
+ equal(addon.id, newId, "Installed add-on has the expected ID");
+
+ yield extension.unload();
+
+ // good only max
+ newId = "strict_max_only@tests.mozilla.org";
+ apps = {
+ applications: {
+ gecko: {
+ id: newId,
+ strict_max_version: "2",
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ yield extension.startup();
+ addon = yield promiseAddonByID(newId);
+
+ notEqual(addon, null, "Add-on is installed");
+ equal(addon.id, newId, "Installed add-on has the expected ID");
+
+ yield extension.unload();
+
+ // * in min will throw an error
+ for (let version of ["0.*", "0.*.0"]) {
+ newId = "strict_min_star@tests.mozilla.org";
+ let minStarApps = {
+ applications: {
+ gecko: {
+ id: newId,
+ strict_min_version: version,
+ },
+ },
+ }
+
+ let minStarTestManifest = Object.assign(minStarApps, MANIFEST);
+
+ let minStarExtension = ExtensionTestUtils.loadExtension({
+ manifest: minStarTestManifest,
+ useAddonManager: "temporary",
+ });
+
+ yield Assert.rejects(
+ minStarExtension.startup(),
+ /The use of '\*' in strict_min_version is invalid/,
+ "loading an extension with a * in strict_min_version throws an exception");
+
+ let minStarAddon = yield promiseAddonByID(newId);
+ equal(minStarAddon, null, "Add-on is not installed");
+ }
+
+ // incompatible extension but with compatibility checking off
+ newId = "checkCompatibility@tests.mozilla.org";
+ apps = {
+ applications: {
+ gecko: {
+ id: newId,
+ strict_max_version: "1",
+ },
+ },
+ }
+ testManifest = Object.assign(apps, MANIFEST);
+
+ extension = ExtensionTestUtils.loadExtension({
+ manifest: testManifest,
+ useAddonManager: "temporary",
+ });
+
+ let savedCheckCompatibilityValue = AddonManager.checkCompatibility;
+ AddonManager.checkCompatibility = false;
+ yield extension.startup();
+ addon = yield promiseAddonByID(newId);
+
+ notEqual(addon, null, "Add-on is installed");
+ equal(addon.id, newId, "Installed add-on has the expected ID");
+
+ yield extension.unload();
+ AddonManager.checkCompatibility = savedCheckCompatibilityValue;
+});
diff --git a/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js
new file mode 100644
index 000000000..29a3dacf7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension_paths.js
@@ -0,0 +1,55 @@
+function run_test() {
+ run_next_test();
+}
+
+let profileDir;
+add_task(function* setup() {
+ profileDir = gProfD.clone();
+ profileDir.append("extensions");
+
+ createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "1.9.2");
+ startupManager();
+});
+
+// When installing an unpacked addon we derive the ID from the
+// directory name. Make sure that if the directoy name is not a valid
+// addon ID that we reject it.
+add_task(function* test_bad_unpacked_path() {
+ let MANIFEST_ID = "webext_bad_path@tests.mozilla.org";
+
+ let manifest = {
+ name: "path test",
+ description: "test of a bad directory name",
+ manifest_version: 2,
+ version: "1.0",
+
+ browser_specific_settings: {
+ gecko: {
+ id: MANIFEST_ID
+ }
+ }
+ };
+
+ const directories = [
+ "not a valid ID",
+ '"quotes"@tests.mozilla.org',
+ ];
+
+ for (let dir of directories) {
+ try {
+ yield promiseWriteWebManifestForExtension(manifest, profileDir, dir);
+ } catch (ex) {
+ // This can fail if the underlying filesystem (looking at you windows)
+ // doesn't handle some of the characters in the ID. In that case,
+ // just ignore this test on this platform.
+ continue;
+ }
+ yield promiseRestartManager();
+
+ let addon = yield promiseAddonByID(dir);
+ do_check_eq(addon, null);
+ addon = yield promiseAddonByID(MANIFEST_ID);
+ do_check_eq(addon, null);
+ }
+});
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
new file mode 100644
index 000000000..1e86e5861
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-shared.ini
@@ -0,0 +1,334 @@
+# The file is shared between the two main xpcshell manifest files.
+[DEFAULT]
+skip-if = toolkit == 'android'
+tags = addons
+
+[test_AddonRepository.js]
+[test_reload.js]
+# Bug 676992: test consistently hangs on Android
+# There's a problem removing a temp file without manually clearing the cache on Windows
+skip-if = os == "android" || os == "win"
+tags = webextensions
+[test_AddonRepository_cache.js]
+# Bug 676992: test consistently hangs on Android
+# Bug 1026805: frequent hangs on OSX 10.8
+skip-if = os == "android" || os == "mac"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_AddonRepository_compatmode.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_LightweightThemeManager.js]
+[test_backgroundupdate.js]
+[test_bad_json.js]
+[test_badschema.js]
+[test_blocklistchange.js]
+# Times out during parallel runs on desktop
+requesttimeoutfactor = 2
+[test_blocklist_prefs.js]
+[test_blocklist_metadata_filters.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_blocklist_regexp.js]
+skip-if = os == "android"
+[test_bootstrap.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bootstrap_const.js]
+[test_bootstrap_resource.js]
+[test_bug299716.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_bug299716_2.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Hardcoded port in install.rdf.
+[test_bug324121.js]
+# Bug 676992: test consistently hangs on Android
+# Bug 1026805: frequent hangs on OSX 10.8
+skip-if = os == "android" || os == "mac"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_bug335238.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_bug371495.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug384052.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug393285.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug394300.js]
+# Bug 676992: test consistently hangs on Android
+# Bug 1026805: frequent hangs on OSX 10.8
+skip-if = os == "android" || os == "mac"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_bug397778.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug406118.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug424262.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug425657.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug430120.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug449027.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug455906.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug465190.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug468528.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_1.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_1_strictcompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_2.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_3.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_3_strictcompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug470377_4.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug514327_1.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug514327_2.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug514327_3.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_bug521905.js]
+[test_bug526598.js]
+[test_bug541420.js]
+[test_bug542391.js]
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_bug554133.js]
+[test_bug559800.js]
+[test_bug563256.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_bug564030.js]
+[test_bug566626.js]
+[test_bug567184.js]
+[test_bug569138.js]
+[test_bug570173.js]
+[test_bug576735.js]
+[test_bug587088.js]
+[test_bug594058.js]
+[test_bug595081.js]
+[test_bug595573.js]
+[test_bug596607.js]
+[test_bug616841.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_bug619730.js]
+[test_bug620837.js]
+[test_bug655254.js]
+[test_bug659772.js]
+[test_bug675371.js]
+[test_bug740612.js]
+[test_bug753900.js]
+[test_bug757663.js]
+[test_bug953156.js]
+[test_checkcompatibility.js]
+[test_checkCompatibility_themeOverride.js]
+[test_childprocess.js]
+[test_ChromeManifestParser.js]
+[test_compatoverrides.js]
+[test_corrupt.js]
+[test_corrupt_strictcompat.js]
+[test_corruptfile.js]
+[test_dataDirectory.js]
+[test_default_providers_pref.js]
+[test_dictionary.js]
+[test_langpack.js]
+[test_disable.js]
+[test_distribution.js]
+[test_dss.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_duplicateplugins.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_error.js]
+[test_experiment.js]
+[test_filepointer.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_fuel.js]
+[test_general.js]
+[test_getresource.js]
+[test_gfxBlacklist_Device.js]
+[test_gfxBlacklist_DriverNew.js]
+[test_gfxBlacklist_Equal_DriverNew.js]
+[test_gfxBlacklist_Equal_DriverOld.js]
+[test_gfxBlacklist_Equal_OK.js]
+[test_gfxBlacklist_GTE_DriverOld.js]
+[test_gfxBlacklist_GTE_OK.js]
+[test_gfxBlacklist_No_Comparison.js]
+[test_gfxBlacklist_OK.js]
+[test_gfxBlacklist_OS.js]
+[test_gfxBlacklist_OSVersion_match.js]
+[test_gfxBlacklist_OSVersion_mismatch_OSVersion.js]
+[test_gfxBlacklist_OSVersion_mismatch_DriverVersion.js]
+[test_gfxBlacklist_Vendor.js]
+[test_gfxBlacklist_Version.js]
+[test_gfxBlacklist_prefs.js]
+# Bug 1248787 - consistently fails
+skip-if = true
+[test_hasbinarycomponents.js]
+[test_hotfix.js]
+[test_install.js]
+[test_install_icons.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_install_strictcompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_isDebuggable.js]
+[test_locale.js]
+[test_locked.js]
+[test_locked2.js]
+[test_locked_strictcompat.js]
+[test_manifest.js]
+[test_mapURIToAddonID.js]
+# Same as test_bootstrap.js
+skip-if = os == "android"
+[test_migrate1.js]
+[test_migrate2.js]
+[test_migrate3.js]
+[test_migrate4.js]
+# Times out during parallel runs on desktop
+requesttimeoutfactor = 2
+[test_migrate5.js]
+[test_migrateAddonRepository.js]
+[test_migrate_max_version.js]
+[test_multiprocessCompatible.js]
+[test_no_addons.js]
+[test_onPropertyChanged_appDisabled.js]
+[test_permissions.js]
+[test_permissions_prefs.js]
+[test_plugins.js]
+[test_pluginchange.js]
+# PluginProvider.jsm is not shipped on Android
+skip-if = os == "android"
+[test_pluginBlocklistCtp.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_pref_properties.js]
+[test_registry.js]
+[test_safemode.js]
+[test_signed_updatepref.js]
+run-if = addon_signing
+skip-if = require_signing
+[test_signed_verify.js]
+run-if = addon_signing
+[test_signed_inject.js]
+run-if = addon_signing
+[test_signed_install.js]
+run-if = addon_signing
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_signed_long.js]
+run-if = addon_signing
+[test_signed_migrate.js]
+run-if = addon_signing
+[test_signed_multi.js]
+run-if = addon_signing
+[test_startup.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_syncGUID.js]
+[test_strictcompatibility.js]
+[test_targetPlatforms.js]
+[test_theme.js]
+# Bug 676992: test consistently fails on Android
+fail-if = os == "android"
+[test_types.js]
+[test_undothemeuninstall.js]
+[test_undouninstall.js]
+[test_uninstall.js]
+[test_update.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_update_webextensions.js]
+tags = webextensions
+[test_updateCancel.js]
+[test_update_strictcompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_update_ignorecompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+[test_updatecheck.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_json_updatecheck.js]
+[test_seen.js]
+[test_seen_newprofile.js]
+[test_updateid.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses hardcoded ports in xpi files.
+[test_update_compatmode.js]
+[test_upgrade.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses global XCurProcD dir.
+[test_upgrade_strictcompat.js]
+# Bug 676992: test consistently hangs on Android
+skip-if = os == "android"
+run-sequentially = Uses global XCurProcD dir.
+[test_overrideblocklist.js]
+run-sequentially = Uses global XCurProcD dir.
+[test_sourceURI.js]
+[test_webextension_icons.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+[test_webextension.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+[test_webextension_install.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+[test_webextension_embedded.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+[test_bootstrap_globals.js]
+[test_bug1180901_2.js]
+skip-if = os != "win"
+[test_bug1180901.js]
+skip-if = os != "win"
+[test_e10s_restartless.js]
+[test_switch_os.js]
+# Bug 1246231
+skip-if = os == "mac" && debug
+[test_softblocked.js]
+[test_ext_management.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
new file mode 100644
index 000000000..42a0ca1ca
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell-unpack.ini
@@ -0,0 +1,12 @@
+ [DEFAULT]
+head = head_addons.js head_unpack.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+dupe-manifest =
+tags = addons
+
+[test_webextension_paths.js]
+tags = webextensions
+
+[include:xpcshell-shared.ini]
diff --git a/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..2b95eb158
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpcshell/xpcshell.ini
@@ -0,0 +1,50 @@
+[DEFAULT]
+skip-if = toolkit == 'android'
+tags = addons
+head = head_addons.js
+tail =
+firefox-appdir = browser
+dupe-manifest =
+support-files =
+ data/**
+ xpcshell-shared.ini
+
+[test_addon_path_service.js]
+[test_asyncBlocklistLoad.js]
+[test_blocklist_gfx.js]
+[test_cache_certdb.js]
+run-if = addon_signing
+[test_cacheflush.js]
+[test_DeferredSave.js]
+[test_gmpProvider.js]
+skip-if = appname != "firefox"
+[test_hotfix_cert.js]
+[test_isReady.js]
+[test_metadata_update.js]
+[test_pluginInfoURL.js]
+[test_provider_markSafe.js]
+[test_provider_shutdown.js]
+[test_provider_unsafe_access_shutdown.js]
+[test_provider_unsafe_access_startup.js]
+[test_ProductAddonChecker.js]
+[test_shutdown.js]
+[test_system_update.js]
+[test_system_reset.js]
+[test_XPIcancel.js]
+[test_XPIStates.js]
+[test_temporary.js]
+tags = webextensions
+[test_install_from_sources.js]
+[test_proxies.js]
+[test_proxy.js]
+[test_pass_symbol.js]
+[test_delay_update.js]
+[test_nodisable_hidden.js]
+[test_delay_update_webextension.js]
+skip-if = appname == "thunderbird"
+tags = webextensions
+[test_dependencies.js]
+[test_schema_change.js]
+[test_system_delay_update.js]
+
+[include:xpcshell-shared.ini]
diff --git a/toolkit/mozapps/extensions/test/xpinstall/.eslintrc.js b/toolkit/mozapps/extensions/test/xpinstall/.eslintrc.js
new file mode 100644
index 000000000..2852eb81d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = { // eslint-disable-line no-undef
+ "extends": [
+ "../../../../../testing/mochitest/browser.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
new file mode 100644
index 000000000..4edf91e34
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/amosigned2.xpi b/toolkit/mozapps/extensions/test/xpinstall/amosigned2.xpi
new file mode 100644
index 000000000..74e877f26
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/amosigned2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs
new file mode 100644
index 000000000..85d448e2b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/authRedirect.sjs
@@ -0,0 +1,21 @@
+// Simple script redirects to the query part of the uri if the browser
+// authenticates with username "testuser" password "testpass"
+
+function handleRequest(request, response) {
+ if (request.hasHeader("Authorization")) {
+ if (request.getHeader("Authorization") == "Basic dGVzdHVzZXI6dGVzdHBhc3M=") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", request.queryString);
+ response.write("See " + request.queryString);
+ }
+ else {
+ response.setStatusLine(request.httpVersion, 403, "Forbidden");
+ response.write("Invalid credentials");
+ }
+ }
+ else {
+ response.setStatusLine(request.httpVersion, 401, "Authentication required");
+ response.setHeader("WWW-Authenticate", "basic realm=\"XPInstall\"", false);
+ response.write("Unauthenticated request");
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser.ini b/toolkit/mozapps/extensions/test/xpinstall/browser.ini
new file mode 100644
index 000000000..5627f47a2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser.ini
@@ -0,0 +1,119 @@
+[DEFAULT]
+support-files =
+ amosigned.xpi
+ amosigned2.xpi
+ authRedirect.sjs
+ bug540558.html
+ bug638292.html
+ bug645699.html
+ concurrent_installs.html
+ cookieRedirect.sjs
+ corrupt.xpi
+ empty.xpi
+ enabled.html
+ hashRedirect.sjs
+ head.js
+ incompatible.xpi
+ installchrome.html
+ installtrigger.html
+ installtrigger_frame.html
+ multipackage.xpi
+ navigate.html
+ redirect.sjs
+ restartless.xpi
+ restartless-unsigned.xpi
+ signed-multipackage.xpi
+ signed-no-cn.xpi
+ signed-no-o.xpi
+ signed-tampered.xpi
+ signed-untrusted.xpi
+ signed.xpi
+ signed2.xpi
+ slowinstall.sjs
+ startsoftwareupdate.html
+ theme.xpi
+ triggerredirect.html
+ unsigned.xpi
+
+[browser_amosigned_trigger.js]
+[browser_amosigned_trigger_iframe.js]
+[browser_amosigned_url.js]
+[browser_auth.js]
+[browser_auth2.js]
+[browser_auth3.js]
+[browser_auth4.js]
+[browser_badargs.js]
+[browser_badargs2.js]
+[browser_badhash.js]
+[browser_badhashtype.js]
+[browser_bug540558.js]
+[browser_bug611242.js]
+[browser_bug638292.js]
+[browser_bug645699.js]
+[browser_bug672485.js]
+skip-if = true # disabled due to a leak. See bug 682410.
+[browser_cancel.js]
+[browser_concurrent_installs.js]
+[browser_cookies.js]
+[browser_cookies2.js]
+[browser_cookies3.js]
+[browser_cookies4.js]
+skip-if = true # Bug 1084646
+[browser_corrupt.js]
+[browser_datauri.js]
+[browser_empty.js]
+[browser_enabled.js]
+[browser_enabled2.js]
+[browser_enabled3.js]
+[browser_hash.js]
+[browser_hash2.js]
+[browser_httphash.js]
+[browser_httphash2.js]
+[browser_httphash3.js]
+[browser_httphash4.js]
+[browser_httphash5.js]
+[browser_httphash6.js]
+[browser_installchrome.js]
+[browser_localfile.js]
+[browser_localfile2.js]
+[browser_localfile3.js]
+[browser_localfile4.js]
+[browser_multipackage.js]
+[browser_navigateaway.js]
+[browser_navigateaway2.js]
+[browser_navigateaway3.js]
+skip-if = (os == "mac" || os == "win") # Bug 1198261
+[browser_navigateaway4.js]
+[browser_offline.js]
+[browser_relative.js]
+[browser_signed_multipackage.js]
+skip-if = require_signing
+[browser_signed_multiple.js]
+skip-if = require_signing
+[browser_signed_naming.js]
+skip-if = require_signing
+[browser_signed_tampered.js]
+skip-if = require_signing
+[browser_signed_trigger.js]
+skip-if = require_signing
+[browser_signed_untrusted.js]
+skip-if = require_signing
+[browser_signed_url.js]
+skip-if = require_signing
+[browser_softwareupdate.js]
+[browser_switchtab.js]
+[browser_trigger_redirect.js]
+[browser_unsigned_trigger.js]
+skip-if = require_signing
+[browser_unsigned_trigger_iframe.js]
+skip-if = require_signing
+[browser_unsigned_trigger_xorigin.js]
+[browser_unsigned_url.js]
+skip-if = require_signing
+[browser_whitelist.js]
+[browser_whitelist2.js]
+[browser_whitelist3.js]
+[browser_whitelist4.js]
+[browser_whitelist5.js]
+[browser_whitelist6.js]
+[browser_whitelist7.js]
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js
new file mode 100644
index 000000000..d61ef7362
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger.js
@@ -0,0 +1,56 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.document.getElementById("return").textContent,
+ status: content.document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "true", "installTrigger should have claimed success");
+ is(results.status, "0", "Callback should have seen a success");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js
new file mode 100644
index 000000000..66564afaa
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_trigger_iframe.js
@@ -0,0 +1,57 @@
+// ----------------------------------------------------------------------------
+// Test for bug 589598 - Ensure that installing through InstallTrigger
+// works in an iframe in web content.
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var inner_url = encodeURIComponent(TESTROOT + "installtrigger.html?" + encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ })));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger_frame.html?" + inner_url);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.frames[0].document.getElementById("return").textContent,
+ status: content.frames[0].document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "true", "installTrigger in iframe should have claimed success");
+ is(results.status, "0", "Callback in iframe should have seen a success");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js
new file mode 100644
index 000000000..f6c39b7bb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_amosigned_url.js
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on by navigating directly to the url
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "amosigned.xpi");
+ });
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have had the filename for the item name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, "", "Should have listed no icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js
new file mode 100644
index 000000000..1bfa7696f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth.js
@@ -0,0 +1,47 @@
+// ----------------------------------------------------------------------------
+// Test whether an install succeeds when authentication is required
+// This verifies bug 312473
+function test() {
+ Harness.authenticationCallback = get_auth_info;
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var prefs = Cc["@mozilla.org/preferences-service;1"].
+ getService(Ci.nsIPrefBranch);
+ prefs.setIntPref("network.auth.subresource-http-auth-allow", 2);
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_auth_info() {
+ return [ "testuser", "testpass" ];
+}
+
+function download_failed(install) {
+ ok(false, "Install should not have failed");
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ var authMgr = Components.classes['@mozilla.org/network/http-auth-manager;1']
+ .getService(Components.interfaces.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js
new file mode 100644
index 000000000..80942a9f4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth2.js
@@ -0,0 +1,46 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when authentication is required and bad
+// credentials are given
+// This verifies bug 312473
+function test() {
+ requestLongerTimeout(2);
+ Harness.authenticationCallback = get_auth_info;
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_auth_info() {
+ return [ "baduser", "badpass" ];
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should have failed");
+}
+
+function install_ended(install, addon) {
+ ok(false, "Add-on should not have installed");
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ var authMgr = Components.classes['@mozilla.org/network/http-auth-manager;1']
+ .getService(Components.interfaces.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js
new file mode 100644
index 000000000..4973a1b35
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth3.js
@@ -0,0 +1,53 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when authentication is required and it is
+// canceled
+// This verifies bug 312473
+
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+
+function test() {
+ Harness.authenticationCallback = get_auth_info;
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_auth_info() {
+ return null;
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should have failed");
+}
+
+function install_ended(install, addon) {
+ ok(false, "Add-on should not have installed");
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ var authMgr = Components.classes['@mozilla.org/network/http-auth-manager;1']
+ .getService(Components.interfaces.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js
new file mode 100644
index 000000000..a3157961d
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_auth4.js
@@ -0,0 +1,52 @@
+//
+// Whitelisting this test.
+// As part of bug 1077403, the leaking uncaught rejection should be fixed.
+//
+thisTestLeaksUncaughtRejectionsAndShouldBeFixed("TypeError: this.docShell is null");
+
+
+// ----------------------------------------------------------------------------
+// Test whether a request for auth for an XPI switches to the appropriate tab
+var gNewTab;
+
+function test() {
+ Harness.authenticationCallback = get_auth_info;
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "authRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gNewTab = gBrowser.addTab();
+ gBrowser.getBrowserForTab(gNewTab).loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_auth_info() {
+ is(gBrowser.selectedTab, gNewTab, "Should have focused the tab loading the XPI");
+ return [ "testuser", "testpass" ];
+}
+
+function download_failed(install) {
+ ok(false, "Install should not have failed");
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ var authMgr = Components.classes['@mozilla.org/network/http-auth-manager;1']
+ .getService(Components.interfaces.nsIHttpAuthManager);
+ authMgr.clearAll();
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeTab(gNewTab);
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js
new file mode 100644
index 000000000..b4b1110b0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Test whether passing a simple string to InstallTrigger.install throws an
+// exception
+function test() {
+ waitForExplicitFinish();
+
+ var triggers = encodeURIComponent(JSON.stringify(TESTROOT + "amosigned.xpi"));
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ return new Promise(resolve => {
+ addEventListener("load", () => {
+ content.addEventListener("InstallTriggered", () => {
+ resolve(content.document.getElementById("return").textContent);
+ });
+ }, true);
+ });
+ }).then(page_loaded);
+
+ // In non-e10s the exception in the content page would trigger a test failure
+ if (!gMultiProcessBrowser)
+ expectUncaughtException();
+
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function page_loaded(result) {
+ is(result, "exception", "installTrigger should have failed");
+
+ // In non-e10s the exception from the page is thrown after the event so we
+ // have to spin the event loop to make sure it arrives so expectUncaughtException
+ // sees it.
+ executeSoon(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js
new file mode 100644
index 000000000..7137df318
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badargs2.js
@@ -0,0 +1,42 @@
+// ----------------------------------------------------------------------------
+// Test whether passing an undefined url InstallTrigger.install throws an
+// exception
+function test() {
+ waitForExplicitFinish();
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: undefined
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, null, function() {
+ return new Promise(resolve => {
+ addEventListener("load", () => {
+ content.addEventListener("InstallTriggered", () => {
+ resolve(content.document.getElementById("return").textContent);
+ });
+ }, true);
+ });
+ }).then(page_loaded);
+
+ // In non-e10s the exception in the content page would trigger a test failure
+ if (!gMultiProcessBrowser)
+ expectUncaughtException();
+
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function page_loaded(result) {
+ is(result, "exception", "installTrigger should have failed");
+
+ // In non-e10s the exception from the page is thrown after the event so we
+ // have to spin the event loop to make sure it arrives so expectUncaughtException
+ // sees it.
+ executeSoon(() => {
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js
new file mode 100644
index 000000000..9a26a77e4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badhash.js
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when an invalid hash is included
+// This verifies bug 302284
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ Hash: "sha1:643b08418599ddbd1ea8a511c90696578fb844b9",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js b/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js
new file mode 100644
index 000000000..a9de9e4f0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_badhashtype.js
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when an unknown hash type is included
+// This verifies bug 302284
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ Hash: "foo:3d0dc22e1f394e159b08aaf5f0f97de4d5c65f4f",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com/"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js
new file mode 100644
index 000000000..4e7e20717
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug540558.js
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------
+// Tests that calling InstallTrigger.installChrome works
+function test() {
+ Harness.installEndedCallback = check_xpi_install;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "bug540558.html");
+}
+
+function check_xpi_install(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js
new file mode 100644
index 000000000..4f3cd087f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug611242.js
@@ -0,0 +1,17 @@
+// ----------------------------------------------------------------------------
+// Test whether setting a new property in InstallTrigger then persists to other
+// page loads
+add_task(function* test() {
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: TESTROOT + "enabled.html" }, function* (browser) {
+ yield ContentTask.spawn(browser, null, () => {
+ content.wrappedJSObject.InstallTrigger.enabled.k = function() { };
+ });
+
+ BrowserTestUtils.loadURI(browser, TESTROOT2 + "enabled.html");
+ yield BrowserTestUtils.browserLoaded(browser);
+ yield ContentTask.spawn(browser, null, () => {
+ is(content.wrappedJSObject.InstallTrigger.enabled.k, undefined, "Property should not be defined");
+ });
+ });
+});
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js
new file mode 100644
index 000000000..0d96e7cbe
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug638292.js
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Test whether an InstallTrigger.enabled is working
+add_task(function * ()
+{
+ let testtab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TESTROOT + "bug638292.html");
+
+ function* verify(link, button)
+ {
+ info("Clicking " + link);
+
+ let waitForNewTabPromise = BrowserTestUtils.waitForNewTab(gBrowser);
+
+ yield BrowserTestUtils.synthesizeMouseAtCenter("#" + link, { button: button },
+ gBrowser.selectedBrowser);
+
+ let newtab = yield waitForNewTabPromise;
+
+ yield BrowserTestUtils.browserLoaded(newtab.linkedBrowser);
+
+ let result = yield ContentTask.spawn(newtab.linkedBrowser, { }, function* () {
+ return (content.document.getElementById("enabled").textContent == "true");
+ });
+
+ ok(result, "installTrigger for " + link + " should have been enabled");
+
+ // Focus the old tab (link3 is opened in the background)
+ if (link != "link3") {
+ yield BrowserTestUtils.switchTab(gBrowser, testtab);
+ }
+ gBrowser.removeTab(newtab);
+ }
+
+ yield* verify("link1", 0);
+ yield* verify("link2", 0);
+ yield* verify("link3", 1);
+
+ gBrowser.removeCurrentTab();
+});
+
+
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
new file mode 100644
index 000000000..f32d2c9d2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug645699.js
@@ -0,0 +1,36 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content. This should be blocked by the whitelist check.
+// This verifies bug 645699
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "bug645699.html");
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, gBrowser.currentURI.spec, "Install should have been triggered by the right uri");
+ return false;
+}
+
+function confirm_install(window) {
+ ok(false, "Should not see the install dialog");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "0 Add-ons should have been successfully installed");
+ Services.perms.remove(makeURI("http://addons.mozilla.org"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js
new file mode 100644
index 000000000..81d89c025
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_bug672485.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+var gWindowWatcher = null;
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installCancelledCallback = cancelled_install;
+ Harness.installEndedCallback = complete_install;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gWindowWatcher = Services.ww;
+ delete Services.ww;
+ is(Services.ww, undefined, "Services.ww should now be undefined");
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ ok(false, "Should not see the install dialog");
+ return false;
+}
+
+function cancelled_install() {
+ ok(true, "Install should b cancelled");
+}
+
+function complete_install() {
+ ok(false, "Install should not have completed");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "0 Add-ons should have been successfully installed");
+
+ gBrowser.removeCurrentTab();
+
+ Services.ww = gWindowWatcher;
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cancel.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cancel.js
new file mode 100644
index 000000000..e30d7a0e5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cancel.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// ----------------------------------------------------------------------------
+// Tests that cancelling multiple installs doesn't fail
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi",
+ "Unsigned XPI 2": TESTROOT + "amosigned2.xpi",
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_item(items, url) {
+ for (let item of items) {
+ if (item.url == url)
+ return item;
+ }
+ ok(false, "Item for " + url + " was not listed");
+ return null;
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 2, "Should be 2 items listed in the confirmation dialog");
+ let item = get_item(items, TESTROOT + "amosigned.xpi");
+ if (item) {
+ is(item.name, "XPI Test", "Should have seen the name from the trigger list");
+ is(item.signed, "false", "Should have listed the item as signed");
+ }
+ item = get_item(items, TESTROOT + "amosigned2.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test", "Should have seen the name from the trigger list");
+ is(item.signed, "false", "Should have listed the item as signed");
+ }
+ return false;
+}
+
+function install_ended(install, addon) {
+ ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_concurrent_installs.js b/toolkit/mozapps/extensions/test/xpinstall/browser_concurrent_installs.js
new file mode 100644
index 000000000..bf919d89c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_concurrent_installs.js
@@ -0,0 +1,127 @@
+// Test that having two frames that request installs at the same time doesn't
+// cause callback ID conflicts (discussed in bug 926712)
+
+var gConcurrentTabs = [];
+var gQueuedForInstall = [];
+var gResults = [];
+
+function frame_script() {
+ /* globals addMessageListener, sendAsyncMessage*/
+ addMessageListener("Test:StartInstall", () => {
+ content.document.getElementById("installnow").click()
+ });
+
+ addEventListener("load", () => {
+ sendAsyncMessage("Test:Loaded");
+
+ content.addEventListener("InstallComplete", (e) => {
+ sendAsyncMessage("Test:InstallComplete", e.detail);
+ }, true);
+ }, true);
+}
+
+var gAddonAndWindowListener = {
+ onOpenWindow: function(win) {
+ var window = win.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindow);
+ info("Window opened");
+
+ waitForFocus(function() {
+ info("Focused!");
+ // Initially the accept button is disabled on a countdown timer
+ let button = window.document.documentElement.getButton("accept");
+ button.disabled = false;
+ if (gQueuedForInstall.length > 0) {
+ // Start downloading the next add-on while we accept this dialog:
+ installNext();
+ }
+ window.document.documentElement.acceptDialog();
+ }, window);
+ },
+ onCloseWindow: function(win) { },
+ onInstallEnded: function(install) {
+ install.cancel();
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediatorListener])
+};
+
+function installNext() {
+ let tab = gQueuedForInstall.shift();
+ tab.linkedBrowser.messageManager.sendAsyncMessage("Test:StartInstall");
+}
+
+function winForTab(t) {
+ return t.linkedBrowser.contentWindow;
+}
+
+function createTab(url) {
+ let tab = gBrowser.addTab(url);
+ tab.linkedBrowser.messageManager.loadFrameScript("data:,(" + frame_script.toString() + ")();", true);
+
+ tab.linkedBrowser.messageManager.addMessageListener("Test:InstallComplete", ({data}) => {
+ gResults.push(data);
+ if (gResults.length == 2) {
+ executeSoon(endThisTest);
+ }
+ });
+
+ return tab;
+}
+
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false);
+ Services.wm.addListener(gAddonAndWindowListener);
+ AddonManager.addInstallListener(gAddonAndWindowListener);
+ registerCleanupFunction(function() {
+ Services.wm.removeListener(gAddonAndWindowListener);
+ AddonManager.removeInstallListener(gAddonAndWindowListener);
+ Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN);
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.perms.remove(makeURI("http://example.org"), "install");
+
+ while (gConcurrentTabs.length) {
+ gBrowser.removeTab(gConcurrentTabs.shift());
+ }
+ });
+
+ let pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+ gConcurrentTabs.push(createTab(TESTROOT + "concurrent_installs.html"));
+ gConcurrentTabs.push(createTab(TESTROOT2 + "concurrent_installs.html"));
+
+ let promises = gConcurrentTabs.map((t) => {
+ return new Promise(resolve => {
+ t.linkedBrowser.messageManager.addMessageListener("Test:Loaded", resolve);
+ });
+ });
+
+ Promise.all(promises).then(() => {
+ gQueuedForInstall = [...gConcurrentTabs];
+ installNext();
+ });
+}
+
+function endThisTest() {
+ is(gResults.length, 2, "Should have two urls");
+ isnot(gResults[0].loc, gResults[1].loc, "Should not have results from the same page.");
+ isnot(gResults[0].xpi, gResults[1].xpi, "Should not have the same XPIs.");
+ for (let i = 0; i < 2; i++) {
+ let {loc, xpi} = gResults[i];
+ if (loc.includes("example.org")) {
+ ok(xpi.includes("example.org"), "Should get .org XPI for .org loc");
+ } else if (loc.includes("example.com")) {
+ ok(xpi.includes("example.com"), "Should get .com XPI for .com loc");
+ } else {
+ ok(false, "Should never get anything that isn't from example.org or example.com");
+ }
+ }
+
+ finish();
+}
+
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js
new file mode 100644
index 000000000..541ac8333
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies.js
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Test that an install that requires cookies to be sent fails when no cookies
+// are set
+// This verifies bug 462739
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Cookie check": TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js
new file mode 100644
index 000000000..1ef2b482f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies2.js
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Test that an install that requires cookies to be sent succeeds when cookies
+// are set
+// This verifies bug 462739
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.add("example.com", "/browser/" + RELATIVE_DIR, "xpinstall", "true", false,
+ false, true, (Date.now() / 1000) + 60, {});
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Cookie check": TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.remove("example.com", "xpinstall", "/browser/" + RELATIVE_DIR, false, {});
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js
new file mode 100644
index 000000000..833562d15
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies3.js
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------
+// Test that an install that requires cookies to be sent succeeds when cookies
+// are set and third party cookies are disabled.
+// This verifies bug 462739
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.add("example.com", "/browser/" + RELATIVE_DIR, "xpinstall", "true", false,
+ false, true, (Date.now() / 1000) + 60, {});
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 1);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Cookie check": TESTROOT + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.remove("example.com", "xpinstall", "/browser/" + RELATIVE_DIR, false, {});
+
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js
new file mode 100644
index 000000000..792146c64
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_cookies4.js
@@ -0,0 +1,43 @@
+// ----------------------------------------------------------------------------
+// Test that an install that requires cookies to be sent fails when cookies
+// are set and third party cookies are disabled and the request is to a third
+// party.
+// This verifies bug 462739
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.add("example.org", "/browser/" + RELATIVE_DIR, "xpinstall", "true", false,
+ false, true, (Date.now() / 1000) + 60, {});
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ Services.prefs.setIntPref("network.cookie.cookieBehavior", 1);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Cookie check": TESTROOT2 + "cookieRedirect.sjs?" + TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_NETWORK_FAILURE, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ var cm = Components.classes["@mozilla.org/cookiemanager;1"]
+ .getService(Components.interfaces.nsICookieManager2);
+ cm.remove("example.org", "xpinstall", "/browser/" + RELATIVE_DIR, false, {});
+
+ Services.prefs.clearUserPref("network.cookie.cookieBehavior");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js b/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js
new file mode 100644
index 000000000..bd4d27fb0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_corrupt.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when the xpi is corrupt.
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Corrupt XPI": TESTROOT + "corrupt.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_CORRUPT_FILE, "Install should fail");
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.document.getElementById("return").textContent,
+ status: content.document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.status, "-207", "Callback should have seen the failure");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js b/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js
new file mode 100644
index 000000000..a8bdbde39
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_datauri.js
@@ -0,0 +1,37 @@
+// ----------------------------------------------------------------------------
+// Checks that a chained redirect through a data URI and javascript is blocked
+
+function setup_redirect(aSettings) {
+ var url = TESTROOT + "redirect.sjs?mode=setup";
+ for (var name in aSettings) {
+ url += "&" + name + "=" + encodeURIComponent(aSettings[name]);
+ }
+
+ var req = new XMLHttpRequest();
+ req.open("GET", url, false);
+ req.send(null);
+}
+
+function test() {
+ Harness.installOriginBlockedCallback = install_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ setup_redirect({
+ "Location": "data:text/html,<script>window.location.href='" + TESTROOT + "amosigned.xpi'</script>"
+ });
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "redirect.sjs?mode=redirect");
+}
+
+function install_blocked(installInfo) {
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js b/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js
new file mode 100644
index 000000000..64ca5e6b9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_empty.js
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when there is no install script present.
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Empty XPI": TESTROOT + "empty.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_CORRUPT_FILE, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js
new file mode 100644
index 000000000..adbec9499
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled.js
@@ -0,0 +1,29 @@
+// ----------------------------------------------------------------------------
+// Test whether an InstallTrigger.enabled is working
+function test() {
+ waitForExplicitFinish();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, TESTROOT + "enabled.html", function (url) {
+ return new Promise(resolve => {
+ function page_loaded() {
+ content.removeEventListener("PageLoaded", page_loaded, false);
+ resolve(content.document.getElementById("enabled").textContent);
+ }
+
+ function load_listener() {
+ removeEventListener("load", load_listener, true);
+ content.addEventListener("PageLoaded", page_loaded, false);
+ }
+
+ addEventListener("load", load_listener, true);
+
+ content.location.href = url;
+ });
+ }).then(text => {
+ is(text, "true", "installTrigger should have been enabled");
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled2.js
new file mode 100644
index 000000000..cc1b8a8b2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled2.js
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------
+// Test whether an InstallTrigger.enabled is working
+function test() {
+ waitForExplicitFinish();
+
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, TESTROOT + "enabled.html", function (url) {
+ return new Promise(resolve => {
+ function page_loaded() {
+ content.removeEventListener("PageLoaded", page_loaded, false);
+ resolve(content.document.getElementById("enabled").textContent);
+ }
+
+ function load_listener() {
+ removeEventListener("load", load_listener, true);
+ content.addEventListener("PageLoaded", page_loaded, false);
+ }
+
+ addEventListener("load", load_listener, true);
+
+ content.location.href = url;
+ });
+ }).then(text => {
+ is(text, "false", "installTrigger should have not been enabled");
+ Services.prefs.clearUserPref("xpinstall.enabled");
+ gBrowser.removeCurrentTab();
+ finish();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_enabled3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled3.js
new file mode 100644
index 000000000..15bad4ba9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_enabled3.js
@@ -0,0 +1,52 @@
+// ----------------------------------------------------------------------------
+// Test whether an InstallTrigger.install call fails when xpinstall is disabled
+function test() {
+ Harness.installDisabledCallback = install_disabled;
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installConfirmCallback = confirm_install;
+ Harness.setup();
+
+ Services.prefs.setBoolPref("xpinstall.enabled", false);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+
+ ContentTask.spawn(gBrowser.selectedBrowser, TESTROOT + "installtrigger.html?" + triggers, url => {
+ return new Promise(resolve => {
+ function page_loaded() {
+ content.removeEventListener("PageLoaded", page_loaded, false);
+ resolve(content.document.getElementById("return").textContent);
+ }
+
+ function load_listener() {
+ removeEventListener("load", load_listener, true);
+ content.addEventListener("InstallTriggered", page_loaded, false);
+ }
+
+ addEventListener("load", load_listener, true);
+
+ content.location.href = url;
+ });
+ }).then(text => {
+ is(text, "false", "installTrigger should have not been enabled");
+ Services.prefs.clearUserPref("xpinstall.enabled");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+ });
+}
+
+function install_disabled(installInfo) {
+ ok(true, "Saw installation disabled");
+}
+
+function allow_blocked(installInfo) {
+ ok(false, "Should never see the blocked install notification");
+ return false;
+}
+
+function confirm_install(window) {
+ ok(false, "Should never see an install confirmation dialog");
+ return false;
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js
new file mode 100644
index 000000000..4c45bd184
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash.js
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------
+// Test whether an install succeeds when a valid hash is included
+// This verifies bug 302284
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ Hash: "sha1:36ffb0acfd9c6e9682473aaebaab394d38b473c9",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js
new file mode 100644
index 000000000..4c8e262c7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_hash2.js
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------
+// Test whether an install succeeds using case-insensitive hashes
+// This verifies bug 603021
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ Hash: "sha1:36FFB0ACFD9C6E9682473AAEBAAB394D38B473C9",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js
new file mode 100644
index 000000000..6a1222797
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash.js
@@ -0,0 +1,39 @@
+// ----------------------------------------------------------------------------
+// Test whether an install succeeds when a valid hash is included in the HTTPS
+// request
+// This verifies bug 591070
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:36ffb0acfd9c6e9682473aaebaab394d38b473c9|" + TESTROOT + "amosigned.xpi";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js
new file mode 100644
index 000000000..2d930d221
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash2.js
@@ -0,0 +1,39 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails when a invalid hash is included in the HTTPS
+// request
+// This verifies bug 591070
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Download should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "0 Add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js
new file mode 100644
index 000000000..5f3ef23ca
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash3.js
@@ -0,0 +1,39 @@
+// ----------------------------------------------------------------------------
+// Tests that the HTTPS hash is ignored when InstallTrigger is passed a hash.
+// This verifies bug 591070
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ Hash: "sha1:36ffb0acfd9c6e9682473aaebaab394d38b473c9",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js
new file mode 100644
index 000000000..3983662af
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash4.js
@@ -0,0 +1,36 @@
+// ----------------------------------------------------------------------------
+// Test that hashes are ignored in the headers of HTTP requests
+// This verifies bug 591070
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var url = "http://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js
new file mode 100644
index 000000000..407158e4f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash5.js
@@ -0,0 +1,40 @@
+// ----------------------------------------------------------------------------
+// Test that only the first HTTPS hash is used
+// This verifies bug 591070
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:36ffb0acfd9c6e9682473aaebaab394d38b473c9|";
+ url += "https://example.com/browser/" + RELATIVE_DIR + "hashRedirect.sjs";
+ url += "?sha1:foobar|" + TESTROOT + "amosigned.xpi";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
new file mode 100644
index 000000000..5e1da8175
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_httphash6.js
@@ -0,0 +1,83 @@
+// ----------------------------------------------------------------------------
+// Tests that a new hash is accepted when restarting a failed download
+// This verifies bug 593535
+function setup_redirect(aSettings) {
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "redirect.sjs?mode=setup";
+ for (var name in aSettings) {
+ url += "&" + name + "=" + aSettings[name];
+ }
+
+ var req = new XMLHttpRequest();
+ req.open("GET", url, false);
+ req.send(null);
+}
+
+var gInstall = null;
+
+function test() {
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_failed_download;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIREBUILTINCERTS, false);
+
+ // Set up the redirect to give a bad hash
+ setup_redirect({
+ "X-Target-Digest": "sha1:foo",
+ "Location": "http://example.com/browser/" + RELATIVE_DIR + "amosigned.xpi"
+ });
+
+ var url = "https://example.com/browser/" + RELATIVE_DIR + "redirect.sjs?mode=redirect";
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: url,
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_INCORRECT_HASH, "Should have seen a hash failure");
+ // Stash the failed download while the harness cleans itself up
+ gInstall = install;
+}
+
+function finish_failed_download() {
+ // Setup to track the successful re-download
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ // Give it the right hash this time
+ setup_redirect({
+ "X-Target-Digest": "sha1:36ffb0acfd9c6e9682473aaebaab394d38b473c9",
+ "Location": "http://example.com/browser/" + RELATIVE_DIR + "amosigned.xpi"
+ });
+
+ // The harness expects onNewInstall events for all installs that are about to start
+ Harness.onNewInstall(gInstall);
+
+ // Restart the install as a regular webpage install so the harness tracks it
+ AddonManager.installAddonsFromWebpage("application/x-xpinstall",
+ gBrowser.selectedBrowser,
+ gBrowser.contentPrincipal, [gInstall]);
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIREBUILTINCERTS);
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js b/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js
new file mode 100644
index 000000000..2380a4df2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_installchrome.js
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------
+// Tests that calling InstallTrigger.installChrome works
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installchrome.html? " + encodeURIComponent(TESTROOT + "amosigned.xpi"));
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
new file mode 100644
index 000000000..997f386a6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile.js
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Tests installing an local file works when loading the url
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+
+ var chromeroot = extractChromeRoot(gTestPath);
+ var xpipath = chromeroot + "unsigned.xpi";
+ try {
+ xpipath = cr.convertChromeURL(makeURI(chromeroot + "amosigned.xpi")).spec;
+ } catch (ex) {
+ // scenario where we are running from a .jar and already extracted
+ }
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(xpipath);
+ });
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js
new file mode 100644
index 000000000..b0e3ffc42
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile2.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Test whether an install fails if the url is a local file when requested from
+// web content
+add_task(function* test() {
+ var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+
+ var chromeroot = getChromeRoot(gTestPath);
+ var xpipath = chromeroot + "amosigned.xpi";
+ try {
+ xpipath = cr.convertChromeURL(makeURI(xpipath)).spec;
+ } catch (ex) {
+ // scenario where we are running from a .jar and already extracted
+ }
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": xpipath
+ }));
+
+ // In non-e10s the exception in the content page would trigger a test failure
+ if (!gMultiProcessBrowser)
+ expectUncaughtException();
+
+ let URI = TESTROOT + "installtrigger.html?" + triggers;
+ yield BrowserTestUtils.withNewTab({ gBrowser, url: "about:blank" }, function* (browser) {
+ yield ContentTask.spawn(browser, URI, function* (URI) {
+ content.location.href = URI;
+
+ let loaded = ContentTaskUtils.waitForEvent(this, "load", true);
+ let installTriggered = ContentTaskUtils.waitForEvent(this, "InstallTriggered", true, null, true);
+ yield Promise.all([ loaded, installTriggered ]);
+
+ let doc = content.document;
+ is(doc.getElementById("return").textContent, "exception", "installTrigger should have failed");
+ });
+ });
+});
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
new file mode 100644
index 000000000..891b85ebc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile3.js
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Tests installing an add-on from a local file with whitelisting disabled.
+// This should be blocked by the whitelist check.
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ // Disable direct request whitelisting, installing from file should be blocked.
+ Services.prefs.setBoolPref("xpinstall.whitelist.directRequest", false);
+
+ var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+
+ var chromeroot = extractChromeRoot(gTestPath);
+ var xpipath = chromeroot + "amosigned.xpi";
+ try {
+ xpipath = cr.convertChromeURL(makeURI(xpipath)).spec;
+ } catch (ex) {
+ // scenario where we are running from a .jar and already extracted
+ }
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(xpipath);
+ });
+}
+
+function allow_blocked(installInfo) {
+ ok(true, "Seen blocked");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+
+ Services.prefs.clearUserPref("xpinstall.whitelist.directRequest");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
new file mode 100644
index 000000000..5203c3153
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_localfile4.js
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Tests installing an add-on from a local file with whitelisting disabled.
+// This should be blocked by the whitelist check.
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ // Disable file request whitelisting, installing by file referrer should be blocked.
+ Services.prefs.setBoolPref("xpinstall.whitelist.fileRequest", false);
+
+ var cr = Components.classes["@mozilla.org/chrome/chrome-registry;1"]
+ .getService(Components.interfaces.nsIChromeRegistry);
+
+ var chromeroot = extractChromeRoot(gTestPath);
+ var xpipath = chromeroot;
+ try {
+ xpipath = cr.convertChromeURL(makeURI(chromeroot)).spec;
+ } catch (ex) {
+ // scenario where we are running from a .jar and already extracted
+ }
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(xpipath + "installtrigger.html?" + triggers);
+}
+
+function allow_blocked(installInfo) {
+ ok(true, "Seen blocked");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+
+ Services.prefs.clearUserPref("xpinstall.whitelist.fileRequest");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js b/toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js
new file mode 100644
index 000000000..d022d713a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_multipackage.js
@@ -0,0 +1,52 @@
+// ----------------------------------------------------------------------------
+// Tests installing an signed add-on by navigating directly to the url
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "multipackage.xpi");
+ });
+}
+
+function get_item(items, name) {
+ for (let item of items) {
+ if (item.name == name)
+ return item;
+ }
+ ok(false, "Item for " + name + " was not listed");
+ return null;
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 2, "Should be 2 items listed in the confirmation dialog");
+
+ let item = get_item(items, "XPI Test");
+ if (item) {
+ is(item.signed, "false", "Should not have listed the item as signed");
+ is(item.icon, "", "Should have listed no icon for the item");
+ }
+
+ item = get_item(items, "Signed XPI Test");
+ if (item) {
+ is(item.signed, "false", "Should have listed the item as signed");
+ is(item.icon, "", "Should have listed no icon for the item");
+ }
+
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 2, "2 Add-ons should have been successfully installed");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js
new file mode 100644
index 000000000..f092edb8c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway.js
@@ -0,0 +1,36 @@
+// ----------------------------------------------------------------------------
+// Tests that navigating away from the initiating page during the install
+// doesn't break the install.
+// This verifies bug 473060
+function test() {
+ Harness.downloadProgressCallback = download_progress;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_progress(addon, value, maxValue) {
+ gBrowser.loadURI(TESTROOT + "enabled.html");
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js
new file mode 100644
index 000000000..a41662e1e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway2.js
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------
+// Tests that closing the initiating page during the install cancels the install
+// to avoid spoofing the user.
+function test() {
+ Harness.downloadProgressCallback = download_progress;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_progress(addon, value, maxValue) {
+ gBrowser.removeCurrentTab();
+}
+
+function install_ended(install, addon) {
+ ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js
new file mode 100644
index 000000000..3948d5649
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway3.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Tests that navigating to a new origin cancels ongoing installs.
+
+// Block the modal install UI from showing.
+Services.prefs.setBoolPref(PREF_CUSTOM_CONFIRMATION_UI, true);
+
+function test() {
+ Harness.downloadProgressCallback = download_progress;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_progress(addon, value, maxValue) {
+ gBrowser.loadURI(TESTROOT2 + "enabled.html");
+}
+
+function install_ended(install, addon) {
+ ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js
new file mode 100644
index 000000000..61844c5a1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_navigateaway4.js
@@ -0,0 +1,44 @@
+// ----------------------------------------------------------------------------
+// Tests that navigating to a new origin cancels ongoing installs and closes
+// the install UI.
+var sawUnload = null;
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ sawUnload = BrowserTestUtils.waitForEvent(window, "unload");
+
+ gBrowser.loadURI(TESTROOT2 + "enabled.html");
+
+ return Harness.leaveOpen;
+}
+
+function install_ended(install, addon) {
+ ok(false, "Should not have seen installs complete");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ sawUnload.then(() => {
+ ok(true, "The install UI should have closed itself.");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+ });
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js
new file mode 100644
index 000000000..eb4ac391f
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_offline.js
@@ -0,0 +1,62 @@
+var proxyPrefValue;
+
+// ----------------------------------------------------------------------------
+// Tests that going offline cancels an in progress download.
+function test() {
+ Harness.downloadProgressCallback = download_progress;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function download_progress(addon, value, maxValue) {
+ try {
+ // Tests always connect to localhost, and per bug 87717, localhost is now
+ // reachable in offline mode. To avoid this, disable any proxy.
+ proxyPrefValue = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref("network.proxy.type", 0);
+ Services.io.manageOfflineStatus = false;
+ Services.io.offline = true;
+ } catch (ex) {
+ }
+}
+
+function finish_test(count) {
+ function wait_for_online() {
+ info("Checking if the browser is still offline...");
+
+ let tab = gBrowser.selectedTab;
+ ContentTask.spawn(tab.linkedBrowser, null, function*() {
+ yield ContentTaskUtils.waitForEvent(this, "DOMContentLoaded", true);
+ return content.document.documentURI;
+ }).then(url => {
+ info("loaded: " + url);
+ if (/^about:neterror\?e=netOffline/.test(url)) {
+ wait_for_online();
+ } else {
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+ }
+ });
+ tab.linkedBrowser.loadURI("http://example.com/");
+ }
+
+ is(count, 0, "No add-ons should have been installed");
+ try {
+ Services.prefs.setIntPref("network.proxy.type", proxyPrefValue);
+ Services.io.offline = false;
+ } catch (ex) {
+ }
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ wait_for_online();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js b/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js
new file mode 100644
index 000000000..3599f47d1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_relative.js
@@ -0,0 +1,55 @@
+// ----------------------------------------------------------------------------
+// Tests that InstallTrigger deals with relative urls correctly.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: "amosigned.xpi",
+ IconURL: "icon.png",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.document.getElementById("return").textContent,
+ status: content.document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "true", "installTrigger should have claimed success");
+ is(results.status, "0", "Callback should have seen a success");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multipackage.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multipackage.js
new file mode 100644
index 000000000..7341f3082
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multipackage.js
@@ -0,0 +1,53 @@
+// ----------------------------------------------------------------------------
+// Tests installing an signed add-on by navigating directly to the url
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "signed-multipackage.xpi");
+ });
+}
+
+function get_item(items, name) {
+ for (let item of items) {
+ if (item.name == name)
+ return item;
+ }
+ ok(false, "Item for " + name + " was not listed");
+ return null;
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 2, "Should be 2 items listed in the confirmation dialog");
+
+ let item = get_item(items, "XPI Test");
+ if (item) {
+ is(item.signed, "false", "Should not have listed the item as signed");
+ is(item.icon, "", "Should have listed no icon for the item");
+ }
+
+ item = get_item(items, "Signed XPI Test");
+ if (item) {
+ is(item.cert, "(Object Signer)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ is(item.icon, "", "Should have listed no icon for the item");
+ }
+
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 2, "2 Add-ons should have been successfully installed");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multiple.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multiple.js
new file mode 100644
index 000000000..9dda1aaeb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_multiple.js
@@ -0,0 +1,72 @@
+// ----------------------------------------------------------------------------
+// Tests installing two signed add-ons in the same trigger works.
+// This verifies bug 453545
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Signed XPI": TESTROOT + "signed.xpi",
+ "Signed XPI 2": TESTROOT + "signed2.xpi",
+ "Signed XPI 3": TESTROOT + "signed-no-o.xpi",
+ "Signed XPI 4": TESTROOT + "signed-no-cn.xpi",
+ "Signed XPI 5": TESTROOT + "unsigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_item(items, url) {
+ for (let item of items) {
+ if (item.url == url)
+ return item;
+ }
+ ok(false, "Item for " + url + " was not listed");
+ return null;
+}
+
+function confirm_install(window) {
+
+ var sbs = Components.classes["@mozilla.org/intl/stringbundle;1"].
+ getService(Components.interfaces.nsIStringBundleService);
+ var bundle = sbs.createBundle("chrome://mozapps/locale/xpinstall/xpinstallConfirm.properties");
+
+ var expectedIntroString = bundle.formatStringFromName("itemWarnIntroMultiple", ["5"], 1);
+
+ var introStringNode = window.document.getElementById("itemWarningIntro");
+ is(introStringNode.textContent, expectedIntroString, "Should have the correct intro string");
+
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 5, "Should be 5 items listed in the confirmation dialog");
+ let item = get_item(items, TESTROOT + "signed.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test", "Should have seen the name from the trigger list");
+ is(item.cert, "(Object Signer)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ }
+ item = get_item(items, TESTROOT + "signed2.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test", "Should have seen the name from the trigger list");
+ is(item.cert, "(Object Signer)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ }
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 5, "5 Add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_naming.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_naming.js
new file mode 100644
index 000000000..5c3f79045
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_naming.js
@@ -0,0 +1,67 @@
+// ----------------------------------------------------------------------------
+// Tests that the correct signer is presented for combinations of O and CN present.
+// The signed files have (when present) O=Mozilla Testing, CN=Object Signer
+// This verifies bug 372980
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Signed XPI (O and CN)": TESTROOT + "signed.xpi",
+ "Signed XPI (CN)": TESTROOT + "signed-no-o.xpi",
+ "Signed XPI (O)": TESTROOT + "signed-no-cn.xpi",
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function get_item(items, url) {
+ for (let item of items) {
+ if (item.url == url)
+ return item;
+ }
+ ok(false, "Item for " + url + " was not listed");
+ return null;
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 3, "Should be 3 items listed in the confirmation dialog");
+ let item = get_item(items, TESTROOT + "signed.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test", "Should have seen the name from the trigger list");
+ is(item.cert, "(Object Signer)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ }
+ item = get_item(items, TESTROOT + "signed-no-o.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test (No Org)", "Should have seen the name from the trigger list");
+ is(item.cert, "(Object Signer)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ }
+ item = get_item(items, TESTROOT + "signed-no-cn.xpi");
+ if (item) {
+ is(item.name, "Signed XPI Test (No Common Name)", "Should have seen the name from the trigger list");
+ is(item.cert, "(Mozilla Testing)", "Should have seen the signer");
+ is(item.signed, "true", "Should have listed the item as signed");
+ }
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 3, "3 Add-ons should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_tampered.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_tampered.js
new file mode 100644
index 000000000..fc798d66a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_tampered.js
@@ -0,0 +1,33 @@
+// ----------------------------------------------------------------------------
+// Tests installing a signed add-on that has been tampered with after signing.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.downloadFailedCallback = download_failed;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Tampered Signed XPI": TESTROOT + "signed-tampered.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ ok(false, "Should not offer to install");
+}
+
+function download_failed(install) {
+ is(install.error, AddonManager.ERROR_CORRUPT_FILE, "Install should fail");
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_trigger.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_trigger.js
new file mode 100644
index 000000000..8f1ae2d87
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_trigger.js
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Tests installing an signed add-on through an InstallTrigger call in web
+// content.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Signed XPI": TESTROOT + "signed.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "Signed XPI Test", "Should have seen the name from the trigger list");
+ is(items[0].url, TESTROOT + "signed.xpi", "Should have listed the correct url for the item");
+ is(items[0].cert, "(Object Signer)", "Should have seen the signer");
+ is(items[0].signed, "true", "Should have listed the item as signed");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_untrusted.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_untrusted.js
new file mode 100644
index 000000000..d5fa9ae01
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_untrusted.js
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Tests installing an add-on signed by an untrusted certificate through an
+// InstallTrigger call in web content.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Untrusted Signed XPI": TESTROOT + "signed-untrusted.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "Signed XPI Test", "Should have had the filename for the item name");
+ is(items[0].url, TESTROOT + "signed-untrusted.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, "", "Should have listed no icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
new file mode 100644
index 000000000..33cda6e4c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_signed_url.js
@@ -0,0 +1,34 @@
+// ----------------------------------------------------------------------------
+// Tests installing an signed add-on by navigating directly to the url
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "signed.xpi");
+ });
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "Signed XPI Test", "Should have had the name");
+ is(items[0].url, TESTROOT + "signed.xpi", "Should have listed the correct url for the item");
+ is(items[0].cert, "(Object Signer)", "Should have seen the signer");
+ is(items[0].signed, "true", "Should have listed the item as signed");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js b/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js
new file mode 100644
index 000000000..16e20f3e9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_softwareupdate.js
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------
+// Tests that calling InstallTrigger.startSoftwareUpdate works
+function test() {
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "startsoftwareupdate.html? " + encodeURIComponent(TESTROOT + "amosigned.xpi"));
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_switchtab.js b/toolkit/mozapps/extensions/test/xpinstall/browser_switchtab.js
new file mode 100644
index 000000000..9bbb59400
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_switchtab.js
@@ -0,0 +1,49 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content.
+var expectedTab = null;
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ }));
+ expectedTab = gBrowser.addTab();
+ expectedTab.linkedBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+
+ is(gBrowser.selectedTab, expectedTab, "Should have switched to the installing tab.");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeTab(expectedTab);
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js b/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js
new file mode 100644
index 000000000..9c0543602
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_trigger_redirect.js
@@ -0,0 +1,41 @@
+// ----------------------------------------------------------------------------
+// Tests that the InstallTrigger callback can redirect to a relative url.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "triggerredirect.html");
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ var doc = gBrowser.contentDocument;
+ is(gBrowser.currentURI.spec, TESTROOT + "triggerredirect.html#foo", "Should have redirected");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js
new file mode 100644
index 000000000..006879439
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger.js
@@ -0,0 +1,56 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content.
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "unsigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.document.getElementById("return").textContent,
+ status: content.document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "true", "installTrigger should have claimed success");
+ is(results.status, "0", "Callback should have seen a success");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
new file mode 100644
index 000000000..b9df6615e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_iframe.js
@@ -0,0 +1,57 @@
+// ----------------------------------------------------------------------------
+// Test for bug 589598 - Ensure that installing through InstallTrigger
+// works in an iframe in web content.
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var inner_url = encodeURIComponent(TESTROOT + "installtrigger.html?" + encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "unsigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ })));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger_frame.html?" + inner_url);
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name");
+ is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, TESTROOT + "icon.png", "Should have listed the correct icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.frames[0].document.getElementById("return").textContent,
+ status: content.frames[0].document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "true", "installTrigger in iframe should have claimed success");
+ is(results.status, "0", "Callback in iframe should have seen a success");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js
new file mode 100644
index 000000000..b0875fc99
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_trigger_xorigin.js
@@ -0,0 +1,38 @@
+// ----------------------------------------------------------------------------
+// Ensure that an inner frame from a different origin can't initiate an install
+
+var wasOriginBlocked = false;
+
+function test() {
+ Harness.installOriginBlockedCallback = install_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.finalContentEvent = "InstallComplete";
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ var inner_url = encodeURIComponent(TESTROOT + "installtrigger.html?" + encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": {
+ URL: TESTROOT + "amosigned.xpi",
+ IconURL: TESTROOT + "icon.png",
+ toString: function() { return this.URL; }
+ }
+ })));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT2 + "installtrigger_frame.html?" + inner_url);
+}
+
+function install_blocked(installInfo) {
+ wasOriginBlocked = true;
+}
+
+function finish_test(count) {
+ ok(wasOriginBlocked, "Should have been blocked due to the cross origin request.");
+
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
new file mode 100644
index 000000000..e103dffd3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_unsigned_url.js
@@ -0,0 +1,35 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on by navigating directly to the url
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "unsigned.xpi");
+ });
+}
+
+function confirm_install(window) {
+ let items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have had the filename for the item name");
+ is(items[0].url, TESTROOT + "unsigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].icon, "", "Should have listed no icon for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+function finish_test(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js
new file mode 100644
index 000000000..ad3d645f5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist.js
@@ -0,0 +1,53 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content. This should be blocked by the whitelist check.
+// This verifies bug 252830
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installEndedCallback = install_ended;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, gBrowser.currentURI.spec, "Install should have been triggered by the right uri");
+ return true;
+}
+
+function confirm_install(window) {
+ var items = window.document.getElementById("itemList").childNodes;
+ is(items.length, 1, "Should only be 1 item listed in the confirmation dialog");
+ is(items[0].name, "XPI Test", "Should have seen the name from the trigger list");
+ is(items[0].url, TESTROOT + "amosigned.xpi", "Should have listed the correct url for the item");
+ is(items[0].signed, "false", "Should have listed the item as unsigned");
+ return true;
+}
+
+function install_ended(install, addon) {
+ install.cancel();
+}
+
+const finish_test = Task.async(function*(count) {
+ is(count, 1, "1 Add-on should have been successfully installed");
+
+ const results = yield ContentTask.spawn(gBrowser.selectedBrowser, null, () => {
+ return {
+ return: content.document.getElementById("return").textContent,
+ status: content.document.getElementById("status").textContent,
+ }
+ })
+
+ is(results.return, "false", "installTrigger should seen a failure");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+});
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist2.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist2.js
new file mode 100644
index 000000000..fd9e944e8
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist2.js
@@ -0,0 +1,31 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an InstallTrigger call in web
+// content. This should be blocked by the whitelist check because the source
+// is not whitelisted, even though the target is.
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+ var triggers = encodeURIComponent(JSON.stringify({
+ "Unsigned XPI": TESTROOT2 + "amosigned.xpi"
+ }));
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installtrigger.html?" + triggers);
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, gBrowser.currentURI.spec, "Install should have been triggered by the right uri");
+ return false;
+}
+
+function finish_test() {
+ Services.perms.remove(makeURI("http://example.org"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js
new file mode 100644
index 000000000..6f4306df6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist3.js
@@ -0,0 +1,28 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through a navigation. Should not be
+// blocked since the referer is whitelisted.
+var url = TESTROOT2 + "navigate.html?" + encodeURIComponent(TESTROOT + "amosigned.xpi");
+
+function test() {
+ Harness.installConfirmCallback = confirm_install;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.org/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(url);
+}
+
+function confirm_install(window) {
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.org"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js
new file mode 100644
index 000000000..e4c47cda5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist4.js
@@ -0,0 +1,30 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through a navigation. Should be
+// blocked since the referer is not whitelisted even though the target is.
+var url = TESTROOT2 + "navigate.html?" + encodeURIComponent(TESTROOT + "amosigned.xpi");
+
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ var pm = Services.perms;
+ pm.add(makeURI("http://example.com/"), "install", pm.ALLOW_ACTION);
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(url);
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, url, "Install should have been triggered by the right uri");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ Services.perms.remove(makeURI("http://example.com"), "install");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist5.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist5.js
new file mode 100644
index 000000000..0a9333430
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist5.js
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through a startSoftwareUpdate call in web
+// content. This should be blocked by the whitelist check.
+// This verifies bug 252830
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "startsoftwareupdate.html? " + encodeURIComponent(TESTROOT + "amosigned.xpi"));
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, gBrowser.currentURI.spec, "Install should have been triggered by the right uri");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist6.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist6.js
new file mode 100644
index 000000000..555f1e2fc
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist6.js
@@ -0,0 +1,25 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through an installChrome call in web
+// content. This should be blocked by the whitelist check.
+// This verifies bug 252830
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ gBrowser.selectedTab = gBrowser.addTab();
+ gBrowser.loadURI(TESTROOT + "installchrome.html? " + encodeURIComponent(TESTROOT + "amosigned.xpi"));
+}
+
+function allow_blocked(installInfo) {
+ is(installInfo.browser, gBrowser.selectedBrowser, "Install should have been triggered by the right browser");
+ is(installInfo.originatingURI.spec, gBrowser.currentURI.spec, "Install should have been triggered by the right uri");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
+// ----------------------------------------------------------------------------
diff --git a/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js
new file mode 100644
index 000000000..96c60ac9e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/browser_whitelist7.js
@@ -0,0 +1,32 @@
+// ----------------------------------------------------------------------------
+// Tests installing an unsigned add-on through a direct install request from
+// web content. This should be blocked by the whitelist check because we disable
+// direct request whitelisting, even though the target URI is whitelisted.
+function test() {
+ Harness.installBlockedCallback = allow_blocked;
+ Harness.installsCompletedCallback = finish_test;
+ Harness.setup();
+
+ // Disable direct request whitelisting, installing should be blocked.
+ Services.prefs.setBoolPref("xpinstall.whitelist.directRequest", false);
+
+ gBrowser.selectedTab = gBrowser.addTab("about:blank");
+ BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser).then(() => {
+ gBrowser.loadURI(TESTROOT + "amosigned.xpi");
+ });
+}
+
+function allow_blocked(installInfo) {
+ ok(true, "Seen blocked");
+ return false;
+}
+
+function finish_test(count) {
+ is(count, 0, "No add-ons should have been installed");
+
+ Services.perms.remove(makeURI("http://example.org"), "install");
+ Services.prefs.clearUserPref("xpinstall.whitelist.directRequest");
+
+ gBrowser.removeCurrentTab();
+ Harness.finish();
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug540558.html b/toolkit/mozapps/extensions/test/xpinstall/bug540558.html
new file mode 100644
index 000000000..286046d25
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/bug540558.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page tests that window.InstallTrigger.install works -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+function startInstall() {
+ window.InstallTrigger.install({
+ "Unsigned XPI": "amosigned.xpi"
+ });
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug638292.html b/toolkit/mozapps/extensions/test/xpinstall/bug638292.html
new file mode 100644
index 000000000..198207d4b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/bug638292.html
@@ -0,0 +1,17 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page tests InstallTrigger is defined in a new window -->
+
+<head>
+<title>InstallTrigger tests</title>
+</head>
+<body>
+<p>InstallTrigger tests</p>
+<p><a id="link1" target="_blank" href="enabled.html">Open window with target</a></p>
+<p><a id="link2" onclick="window.open(this.href); return false" href="enabled.html">Open window with JS</a></p>
+<p><a id="link3" href="enabled.html">Open window with middle-click</a></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/bug645699.html b/toolkit/mozapps/extensions/test/xpinstall/bug645699.html
new file mode 100644
index 000000000..8334c37c2
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/bug645699.html
@@ -0,0 +1,31 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function startInstall() {
+ var whiteUrl = "https://example.org/";
+
+ try {
+ Object.defineProperty(window, "location", { value : { href : whiteUrl } });
+ throw new Error("Object.defineProperty(window, 'location', ...) should have thrown");
+ } catch (exc) {
+ if (!(exc instanceof TypeError))
+ throw exc;
+ }
+ Object.defineProperty(document, "documentURIObject", { spec : { href : whiteUrl } });
+
+ InstallTrigger.install({
+ "Unsigned XPI": "http://example.com/browser/toolkit/mozapps/extensions/test/xpinstall/amosigned.xpi"
+ });
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/concurrent_installs.html b/toolkit/mozapps/extensions/test/xpinstall/concurrent_installs.html
new file mode 100644
index 000000000..780409257
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/concurrent_installs.html
@@ -0,0 +1,40 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<head>
+ <meta charset="utf-8">
+<title>Concurrent InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function installCallback(url, status) {
+ document.getElementById("status").textContent = status;
+
+ dump("Sending InstallComplete\n");
+ var event = new CustomEvent("InstallComplete", {detail: {loc: location.href, xpi: url}});
+ window.dispatchEvent(event);
+}
+
+function startInstall() {
+ var root = location.href.replace("concurrent_installs.html", "");
+ var triggers = {
+ "Unsigned XPI": root + "amosigned.xpi"
+ };
+ try {
+ document.getElementById("return").textContent = InstallTrigger.install(triggers, installCallback);
+ }
+ catch (e) {
+ document.getElementById("return").textContent = "exception";
+ throw e;
+ }
+}
+</script>
+</head>
+<body>
+<p>InstallTrigger tests</p>
+<button id="installnow" onclick="startInstall()">Click to install</button>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs
new file mode 100644
index 000000000..92bccd9ec
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/cookieRedirect.sjs
@@ -0,0 +1,24 @@
+// Simple script redirects to the query part of the uri if the cookie "xpinstall"
+// has the value "true", otherwise gives a 500 error.
+
+function handleRequest(request, response)
+{
+ let cookie = null;
+ if (request.hasHeader("Cookie")) {
+ let cookies = request.getHeader("Cookie").split(";");
+ for (let i = 0; i < cookies.length; i++) {
+ if (cookies[i].substring(0, 10) == "xpinstall=")
+ cookie = cookies[i].substring(10);
+ }
+ }
+
+ if (cookie == "true") {
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("Location", request.queryString);
+ response.write("See " + request.queryString);
+ }
+ else {
+ response.setStatusLine(request.httpVersion, 500, "Internal Server Error");
+ response.write("Invalid request");
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi b/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
new file mode 100644
index 000000000..35d7bd5e5
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/corrupt.xpi
@@ -0,0 +1 @@
+This is a corrupt zip file
diff --git a/toolkit/mozapps/extensions/test/xpinstall/empty.xpi b/toolkit/mozapps/extensions/test/xpinstall/empty.xpi
new file mode 100644
index 000000000..74ed2b817
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/empty.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/enabled.html b/toolkit/mozapps/extensions/test/xpinstall/enabled.html
new file mode 100644
index 000000000..370cde8fb
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/enabled.html
@@ -0,0 +1,24 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will test if InstallTrigger seems to be enabled -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function init() {
+ document.getElementById("enabled").textContent = InstallTrigger.enabled() ? "true" : "false";
+ dump("Sending PageLoaded\n");
+ var event = new CustomEvent("PageLoaded");
+ window.dispatchEvent(event);
+}
+</script>
+</head>
+<body onload="init()">
+<p>InstallTrigger tests</p>
+<p id="enabled"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs
new file mode 100644
index 000000000..324a092a3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/hashRedirect.sjs
@@ -0,0 +1,15 @@
+// Simple script redirects takes the query part of te request and splits it on
+// the | character. Anything before is included as the X-Target-Digest header
+// the latter part is used as the url to redirect to
+
+function handleRequest(request, response)
+{
+ let pos = request.queryString.indexOf("|");
+ let header = request.queryString.substring(0, pos);
+ let url = request.queryString.substring(pos + 1);
+
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ response.setHeader("X-Target-Digest", header);
+ response.setHeader("Location", url);
+ response.write("See " + url);
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/head.js b/toolkit/mozapps/extensions/test/xpinstall/head.js
new file mode 100644
index 000000000..197fe3fac
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/head.js
@@ -0,0 +1,434 @@
+const RELATIVE_DIR = "toolkit/mozapps/extensions/test/xpinstall/";
+
+const TESTROOT = "http://example.com/browser/" + RELATIVE_DIR;
+const TESTROOT2 = "http://example.org/browser/" + RELATIVE_DIR;
+const XPINSTALL_URL = "chrome://mozapps/content/xpinstall/xpinstallConfirm.xul";
+const PROMPT_URL = "chrome://global/content/commonDialog.xul";
+const ADDONS_URL = "chrome://mozapps/content/extensions/extensions.xul";
+const PREF_LOGGING_ENABLED = "extensions.logging.enabled";
+const PREF_INSTALL_REQUIREBUILTINCERTS = "extensions.install.requireBuiltInCerts";
+const PREF_INSTALL_REQUIRESECUREORIGIN = "extensions.install.requireSecureOrigin";
+const PREF_CUSTOM_CONFIRMATION_UI = "xpinstall.customConfirmationUI";
+const CHROME_NAME = "mochikit";
+
+function getChromeRoot(path) {
+ if (path === undefined) {
+ return "chrome://" + CHROME_NAME + "/content/browser/" + RELATIVE_DIR
+ }
+ return getRootDirectory(path);
+}
+
+function extractChromeRoot(path) {
+ var chromeRootPath = getChromeRoot(path);
+ var jar = getJar(chromeRootPath);
+ if (jar) {
+ var tmpdir = extractJarToTmp(jar);
+ return "file://" + tmpdir.path + "/";
+ }
+ return chromeRootPath;
+}
+
+Services.prefs.setBoolPref(PREF_CUSTOM_CONFIRMATION_UI, false);
+registerCleanupFunction(() => {
+ Services.prefs.clearUserPref(PREF_CUSTOM_CONFIRMATION_UI);
+});
+
+/**
+ * This is a test harness designed to handle responding to UI during the process
+ * of installing an XPI. A test can set callbacks to hear about specific parts
+ * of the sequence.
+ * Before use setup must be called and finish must be called afterwards.
+ */
+var Harness = {
+ // If set then the callback is called when an install is attempted and
+ // software installation is disabled.
+ installDisabledCallback: null,
+ // If set then the callback is called when an install is attempted and
+ // then canceled.
+ installCancelledCallback: null,
+ // If set then the callback will be called when an install's origin is blocked.
+ installOriginBlockedCallback: null,
+ // If set then the callback will be called when an install is blocked by the
+ // whitelist. The callback should return true to continue with the install
+ // anyway.
+ installBlockedCallback: null,
+ // If set will be called in the event of authentication being needed to get
+ // the xpi. Should return a 2 element array of username and password, or
+ // null to not authenticate.
+ authenticationCallback: null,
+ // If set this will be called to allow checking the contents of the xpinstall
+ // confirmation dialog. The callback should return true to continue the install.
+ installConfirmCallback: null,
+ // If set will be called when downloading of an item has begun.
+ downloadStartedCallback: null,
+ // If set will be called during the download of an item.
+ downloadProgressCallback: null,
+ // If set will be called when an xpi fails to download.
+ downloadFailedCallback: null,
+ // If set will be called when an xpi download is cancelled.
+ downloadCancelledCallback: null,
+ // If set will be called when downloading of an item has ended.
+ downloadEndedCallback: null,
+ // If set will be called when installation by the extension manager of an xpi
+ // item starts
+ installStartedCallback: null,
+ // If set will be called when an xpi fails to install.
+ installFailedCallback: null,
+ // If set will be called when each xpi item to be installed completes
+ // installation.
+ installEndedCallback: null,
+ // If set will be called when all triggered items are installed or the install
+ // is canceled.
+ installsCompletedCallback: null,
+ // If set the harness will wait for this DOM event before calling
+ // installsCompletedCallback
+ finalContentEvent: null,
+
+ waitingForEvent: false,
+ pendingCount: null,
+ installCount: null,
+ runningInstalls: null,
+
+ waitingForFinish: false,
+
+ // A unique value to return from the installConfirmCallback to indicate that
+ // the install UI shouldn't be closed automatically
+ leaveOpen: {},
+
+ // Setup and tear down functions
+ setup: function() {
+ if (!this.waitingForFinish) {
+ waitForExplicitFinish();
+ this.waitingForFinish = true;
+
+ Services.prefs.setBoolPref(PREF_INSTALL_REQUIRESECUREORIGIN, false);
+
+ Services.prefs.setBoolPref(PREF_LOGGING_ENABLED, true);
+ Services.obs.addObserver(this, "addon-install-started", false);
+ Services.obs.addObserver(this, "addon-install-disabled", false);
+ Services.obs.addObserver(this, "addon-install-origin-blocked", false);
+ Services.obs.addObserver(this, "addon-install-blocked", false);
+ Services.obs.addObserver(this, "addon-install-failed", false);
+ Services.obs.addObserver(this, "addon-install-complete", false);
+
+ AddonManager.addInstallListener(this);
+
+ Services.wm.addListener(this);
+
+ var self = this;
+ registerCleanupFunction(function() {
+ Services.prefs.clearUserPref(PREF_LOGGING_ENABLED);
+ Services.prefs.clearUserPref(PREF_INSTALL_REQUIRESECUREORIGIN);
+ Services.obs.removeObserver(self, "addon-install-started");
+ Services.obs.removeObserver(self, "addon-install-disabled");
+ Services.obs.removeObserver(self, "addon-install-origin-blocked");
+ Services.obs.removeObserver(self, "addon-install-blocked");
+ Services.obs.removeObserver(self, "addon-install-failed");
+ Services.obs.removeObserver(self, "addon-install-complete");
+
+ AddonManager.removeInstallListener(self);
+
+ Services.wm.removeListener(self);
+
+ AddonManager.getAllInstalls(function(aInstalls) {
+ is(aInstalls.length, 0, "Should be no active installs at the end of the test");
+ aInstalls.forEach(function(aInstall) {
+ info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state);
+ aInstall.cancel();
+ });
+ });
+ });
+ }
+
+ this.installCount = 0;
+ this.pendingCount = 0;
+ this.runningInstalls = [];
+ },
+
+ finish: function() {
+ finish();
+ },
+
+ endTest: function() {
+ let callback = this.installsCompletedCallback;
+ let count = this.installCount;
+
+ is(this.runningInstalls.length, 0, "Should be no running installs left");
+ this.runningInstalls.forEach(function(aInstall) {
+ info("Install for " + aInstall.sourceURI + " is in state " + aInstall.state);
+ });
+
+ this.installOriginBlockedCallback = null;
+ this.installBlockedCallback = null;
+ this.authenticationCallback = null;
+ this.installConfirmCallback = null;
+ this.downloadStartedCallback = null;
+ this.downloadProgressCallback = null;
+ this.downloadCancelledCallback = null;
+ this.downloadFailedCallback = null;
+ this.downloadEndedCallback = null;
+ this.installStartedCallback = null;
+ this.installFailedCallback = null;
+ this.installEndedCallback = null;
+ this.installsCompletedCallback = null;
+ this.runningInstalls = null;
+
+ if (callback)
+ executeSoon(() => callback(count));
+ },
+
+ // Window open handling
+ windowReady: function(window) {
+ if (window.document.location.href == XPINSTALL_URL) {
+ if (this.installBlockedCallback)
+ ok(false, "Should have been blocked by the whitelist");
+ this.pendingCount = window.document.getElementById("itemList").childNodes.length;
+
+ // If there is a confirm callback then its return status determines whether
+ // to install the items or not. If not the test is over.
+ let result = true;
+ if (this.installConfirmCallback) {
+ result = this.installConfirmCallback(window);
+ if (result === this.leaveOpen)
+ return;
+ }
+
+ if (!result) {
+ window.document.documentElement.cancelDialog();
+ }
+ else {
+ // Initially the accept button is disabled on a countdown timer
+ var button = window.document.documentElement.getButton("accept");
+ button.disabled = false;
+ window.document.documentElement.acceptDialog();
+ }
+ }
+ else if (window.document.location.href == PROMPT_URL) {
+ var promptType = window.args.promptType;
+ switch (promptType) {
+ case "alert":
+ case "alertCheck":
+ case "confirmCheck":
+ case "confirm":
+ case "confirmEx":
+ window.document.documentElement.acceptDialog();
+ break;
+ case "promptUserAndPass":
+ // This is a login dialog, hopefully an authentication prompt
+ // for the xpi.
+ if (this.authenticationCallback) {
+ var auth = this.authenticationCallback();
+ if (auth && auth.length == 2) {
+ window.document.getElementById("loginTextbox").value = auth[0];
+ window.document.getElementById("password1Textbox").value = auth[1];
+ window.document.documentElement.acceptDialog();
+ }
+ else {
+ window.document.documentElement.cancelDialog();
+ }
+ }
+ else {
+ window.document.documentElement.cancelDialog();
+ }
+ break;
+ default:
+ ok(false, "prompt type " + promptType + " not handled in test.");
+ break;
+ }
+ }
+ },
+
+ // Install blocked handling
+
+ installDisabled: function(installInfo) {
+ ok(!!this.installDisabledCallback, "Installation shouldn't have been disabled");
+ if (this.installDisabledCallback)
+ this.installDisabledCallback(installInfo);
+ this.expectingCancelled = true;
+ this.expectingCancelled = false;
+ this.endTest();
+ },
+
+ installCancelled: function(installInfo) {
+ if (this.expectingCancelled)
+ return;
+
+ ok(!!this.installCancelledCallback, "Installation shouldn't have been cancelled");
+ if (this.installCancelledCallback)
+ this.installCancelledCallback(installInfo);
+ this.endTest();
+ },
+
+ installOriginBlocked: function(installInfo) {
+ ok(!!this.installOriginBlockedCallback, "Shouldn't have been blocked");
+ if (this.installOriginBlockedCallback)
+ this.installOriginBlockedCallback(installInfo);
+ this.endTest();
+ },
+
+ installBlocked: function(installInfo) {
+ ok(!!this.installBlockedCallback, "Shouldn't have been blocked by the whitelist");
+ if (this.installBlockedCallback && this.installBlockedCallback(installInfo)) {
+ this.installBlockedCallback = null;
+ installInfo.install();
+ }
+ else {
+ this.expectingCancelled = true;
+ installInfo.installs.forEach(function(install) {
+ install.cancel();
+ });
+ this.expectingCancelled = false;
+ this.endTest();
+ }
+ },
+
+ // nsIWindowMediatorListener
+
+ onWindowTitleChange: function(window, title) {
+ },
+
+ onOpenWindow: function(window) {
+ var domwindow = window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+ .getInterface(Components.interfaces.nsIDOMWindow);
+ var self = this;
+ waitForFocus(function() {
+ self.windowReady(domwindow);
+ }, domwindow);
+ },
+
+ onCloseWindow: function(window) {
+ },
+
+ // Addon Install Listener
+
+ onNewInstall: function(install) {
+ this.runningInstalls.push(install);
+
+ if (this.finalContentEvent && !this.waitingForEvent) {
+ this.waitingForEvent = true;
+ info("Waiting for " + this.finalContentEvent);
+ let mm = gBrowser.selectedBrowser.messageManager;
+ mm.loadFrameScript(`data:,content.addEventListener("${this.finalContentEvent}", () => { sendAsyncMessage("Test:GotNewInstallEvent"); });`, false);
+ let win = gBrowser.contentWindow;
+ let listener = () => {
+ info("Saw " + this.finalContentEvent);
+ mm.removeMessageListener("Test:GotNewInstallEvent", listener);
+ this.waitingForEvent = false;
+ if (this.pendingCount == 0)
+ this.endTest();
+ }
+ mm.addMessageListener("Test:GotNewInstallEvent", listener);
+ }
+ },
+
+ onDownloadStarted: function(install) {
+ this.pendingCount++;
+ if (this.downloadStartedCallback)
+ this.downloadStartedCallback(install);
+ },
+
+ onDownloadProgress: function(install) {
+ if (this.downloadProgressCallback)
+ this.downloadProgressCallback(install);
+ },
+
+ onDownloadEnded: function(install) {
+ if (this.downloadEndedCallback)
+ this.downloadEndedCallback(install);
+ },
+
+ onDownloadCancelled: function(install) {
+ isnot(this.runningInstalls.indexOf(install), -1,
+ "Should only see cancelations for started installs");
+ this.runningInstalls.splice(this.runningInstalls.indexOf(install), 1);
+
+ if (this.downloadCancelledCallback)
+ this.downloadCancelledCallback(install);
+ this.checkTestEnded();
+ },
+
+ onDownloadFailed: function(install) {
+ if (this.downloadFailedCallback)
+ this.downloadFailedCallback(install);
+ this.checkTestEnded();
+ },
+
+ onInstallStarted: function(install) {
+ if (this.installStartedCallback)
+ this.installStartedCallback(install);
+ },
+
+ onInstallEnded: function(install, addon) {
+ if (this.installEndedCallback)
+ this.installEndedCallback(install, addon);
+ this.installCount++;
+ this.checkTestEnded();
+ },
+
+ onInstallFailed: function(install) {
+ if (this.installFailedCallback)
+ this.installFailedCallback(install);
+ this.checkTestEnded();
+ },
+
+ checkTestEnded: function() {
+ if (--this.pendingCount == 0 && !this.waitingForEvent)
+ this.endTest();
+ },
+
+ // nsIObserver
+
+ observe: function(subject, topic, data) {
+ var installInfo = subject.QueryInterface(Components.interfaces.amIWebInstallInfo);
+ switch (topic) {
+ case "addon-install-started":
+ is(this.runningInstalls.length, installInfo.installs.length,
+ "Should have seen the expected number of installs started");
+ break;
+ case "addon-install-disabled":
+ this.installDisabled(installInfo);
+ break;
+ case "addon-install-cancelled":
+ this.installCancelled(installInfo);
+ break;
+ case "addon-install-origin-blocked":
+ this.installOriginBlocked(installInfo);
+ break;
+ case "addon-install-blocked":
+ this.installBlocked(installInfo);
+ break;
+ case "addon-install-failed":
+ installInfo.installs.forEach(function(aInstall) {
+ isnot(this.runningInstalls.indexOf(aInstall), -1,
+ "Should only see failures for started installs");
+
+ ok(aInstall.error != 0 || aInstall.addon.appDisabled,
+ "Failed installs should have an error or be appDisabled");
+
+ this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1);
+ }, this);
+ break;
+ case "addon-install-complete":
+ installInfo.installs.forEach(function(aInstall) {
+ isnot(this.runningInstalls.indexOf(aInstall), -1,
+ "Should only see completed events for started installs");
+
+ is(aInstall.error, 0, "Completed installs should have no error");
+ ok(!aInstall.appDisabled, "Completed installs should not be appDisabled");
+
+ // Complete installs are either in the INSTALLED or CANCELLED state
+ // since the test may cancel installs the moment they complete.
+ ok(aInstall.state == AddonManager.STATE_INSTALLED ||
+ aInstall.state == AddonManager.STATE_CANCELLED,
+ "Completed installs should be in the right state");
+
+ this.runningInstalls.splice(this.runningInstalls.indexOf(aInstall), 1);
+ }, this);
+ break;
+ }
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
+ Ci.nsIWindowMediatorListener,
+ Ci.nsISupports])
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi b/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
new file mode 100644
index 000000000..262ed38a7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/incompatible.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/installchrome.html b/toolkit/mozapps/extensions/test/xpinstall/installchrome.html
new file mode 100644
index 000000000..6abee2ef3
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/installchrome.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept a url as the uri query and pass it to InstallTrigger.installChrome -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function startInstall() {
+ InstallTrigger.installChrome(InstallTrigger.SKIN,
+ decodeURIComponent(document.location.search.substring(1)),
+ "test");
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html b/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
new file mode 100644
index 000000000..65cab1ef1
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger.html
@@ -0,0 +1,44 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept some json as the uri query and pass it to InstallTrigger.install -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function installCallback(url, status) {
+ document.getElementById("status").textContent = status;
+
+ dump("Sending InstallComplete\n");
+ var event = new CustomEvent("InstallComplete");
+ var target = window.parent ? window.parent : window;
+ target.dispatchEvent(event);
+}
+
+function startInstall() {
+ var event = new CustomEvent("InstallTriggered");
+ var text = decodeURIComponent(document.location.search.substring(1));
+ var triggers = JSON.parse(text);
+ try {
+ document.getElementById("return").textContent = InstallTrigger.install(triggers, installCallback);
+ dump("Sending InstallTriggered\n");
+ window.dispatchEvent(event);
+ }
+ catch (e) {
+ document.getElementById("return").textContent = "exception";
+ dump("Sending InstallTriggered\n");
+ window.dispatchEvent(event);
+ throw e;
+ }
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
new file mode 100644
index 000000000..2b302642e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/installtrigger_frame.html
@@ -0,0 +1,29 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept some url as the uri query and load it in
+ an inner iframe, which will run InstallTrigger.install -->
+
+<head>
+<title>InstallTrigger frame tests</title>
+<script type="text/javascript">
+function prepChild() {
+ // Pass our parameters over to the child
+ var child = window.frames[0];
+ var url = decodeURIComponent(document.location.search.substr(1));
+ child.location = url;
+}
+</script>
+</head>
+<body onload="prepChild()">
+
+<iframe src="about:blank">
+</iframe>
+
+<p>InstallTrigger tests</p>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/multipackage.xpi b/toolkit/mozapps/extensions/test/xpinstall/multipackage.xpi
new file mode 100644
index 000000000..d52f28c28
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/multipackage.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/navigate.html b/toolkit/mozapps/extensions/test/xpinstall/navigate.html
new file mode 100644
index 000000000..5a6903eb9
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/navigate.html
@@ -0,0 +1,26 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept some url as the uri query and navigate to it by
+ clicking a link -->
+
+<head>
+<title>Navigation tests</title>
+<script type="text/javascript">
+function navigate() {
+ // Pass our parameters over to the child
+ var child = window.frames[0];
+ var url = decodeURIComponent(document.location.search.substr(1));
+ var link = document.getElementById("link");
+ link.href = url;
+ link.click();
+}
+</script>
+</head>
+<body onload="navigate()">
+
+<p><a id="link">Test Link</a></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs b/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
new file mode 100644
index 000000000..d248bfbc7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/redirect.sjs
@@ -0,0 +1,45 @@
+// Script has two modes based on the query string. If the mode is "setup" then
+// parameters from the query string configure the redirection. If the mode is
+// "redirect" then a redirect is returned
+
+function handleRequest(request, response)
+{
+ let parts = request.queryString.split("&");
+ let settings = {};
+
+ parts.forEach(function(aString) {
+ let [k, v] = aString.split("=");
+ settings[k] = decodeURIComponent(v);
+ })
+
+ if (settings.mode == "setup") {
+ delete settings.mode;
+
+ // Object states must be an nsISupports
+ var state = {
+ settings: settings,
+ QueryInterface: function(aIid) {
+ if (aIid.equals(Components.interfaces.nsISupports))
+ return settings;
+ throw Components.results.NS_ERROR_NO_INTERFACE;
+ }
+ }
+ state.wrappedJSObject = state;
+
+ setObjectState("xpinstall-redirect-settings", state);
+ response.setStatusLine(request.httpVersion, 200, "Ok");
+ response.setHeader("Content-Type", "text/plain");
+ response.write("Setup complete");
+ }
+ else if (settings.mode == "redirect") {
+ getObjectState("xpinstall-redirect-settings", function(aObject) {
+ settings = aObject.wrappedJSObject.settings;
+ });
+
+ response.setStatusLine(request.httpVersion, 302, "Found");
+ for (var name in settings) {
+ response.setHeader(name, settings[name]);
+ }
+ response.write("Done");
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
new file mode 100644
index 000000000..8e76bd052
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/restartless-unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
new file mode 100644
index 000000000..9fee8f60b
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/restartless.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed-multipackage.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed-multipackage.xpi
new file mode 100644
index 000000000..11fbe1861
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed-multipackage.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed-no-cn.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed-no-cn.xpi
new file mode 100644
index 000000000..90d3a3ce6
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed-no-cn.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed-no-o.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed-no-o.xpi
new file mode 100644
index 000000000..19b754038
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed-no-o.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed-tampered.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed-tampered.xpi
new file mode 100644
index 000000000..8c951881e
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed-tampered.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed-untrusted.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed-untrusted.xpi
new file mode 100644
index 000000000..09789d189
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed-untrusted.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed.xpi
new file mode 100644
index 000000000..bd7f78b7c
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/signed2.xpi b/toolkit/mozapps/extensions/test/xpinstall/signed2.xpi
new file mode 100644
index 000000000..085efbbf7
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/signed2.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs b/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
new file mode 100644
index 000000000..5f767a8f4
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/slowinstall.sjs
@@ -0,0 +1,101 @@
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+const RELATIVE_PATH = "browser/toolkit/mozapps/extensions/test/xpinstall"
+const NOTIFICATION_TOPIC = "slowinstall-complete";
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param aQueryString
+ * The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ * queryString.
+ */
+function parseQueryString(aQueryString) {
+ var paramArray = aQueryString.split("&");
+ var regex = /^([^=]+)=(.*)$/;
+ var params = {};
+ for (var i = 0, sz = paramArray.length; i < sz; i++) {
+ var match = regex.exec(paramArray[i]);
+ if (!match)
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+function handleRequest(aRequest, aResponse) {
+ let id = +getState("ID");
+ setState("ID", "" + (id + 1));
+
+ function LOG(str) {
+ dump("slowinstall.sjs[" + id + "]: " + str + "\n");
+ }
+
+ aResponse.setStatusLine(aRequest.httpVersion, 200, "OK");
+
+ var params = { };
+ if (aRequest.queryString)
+ params = parseQueryString(aRequest.queryString);
+
+ if (params.file) {
+ let xpiFile = "";
+
+ function complete_download() {
+ LOG("Completing download");
+ downloadPaused = false;
+
+ try {
+ // Doesn't seem to be a sane way to read using OS.File and write to an
+ // nsIOutputStream so here we are.
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(xpiFile);
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ stream.init(file, -1, -1, stream.DEFER_OPEN + stream.CLOSE_ON_EOF);
+
+ NetUtil.asyncCopy(stream, aResponse.bodyOutputStream, () => {
+ LOG("Download complete");
+ aResponse.finish();
+ });
+ }
+ catch (e) {
+ LOG("Exception " + e);
+ }
+ }
+
+ let waitForComplete = new Promise(resolve => {
+ function complete() {
+ Services.obs.removeObserver(complete, NOTIFICATION_TOPIC);
+ resolve();
+ }
+
+ Services.obs.addObserver(complete, NOTIFICATION_TOPIC, false);
+ });
+
+ aResponse.processAsync();
+
+ OS.File.getCurrentDirectory().then(dir => {
+ xpiFile = OS.Path.join(dir, ...RELATIVE_PATH.split("/"), params.file);
+ LOG("Starting slow download of " + xpiFile);
+
+ OS.File.stat(xpiFile).then(info => {
+ aResponse.setHeader("Content-Type", "binary/octet-stream");
+ aResponse.setHeader("Content-Length", info.size.toString());
+
+ LOG("Download paused");
+ waitForComplete.then(complete_download);
+ });
+ });
+ }
+ else if (params.continue) {
+ dump("slowinstall.sjs: Received signal to complete all current downloads.\n");
+ Services.obs.notifyObservers(null, NOTIFICATION_TOPIC, null);
+ }
+}
diff --git a/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html b/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html
new file mode 100644
index 000000000..50083ca90
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/startsoftwareupdate.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will accept a url as the uri query and pass it to InstallTrigger.startSoftwareUpdate -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function startInstall() {
+ InstallTrigger.startSoftwareUpdate(decodeURIComponent(document.location.search.substring(1)));
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/theme.xpi b/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
new file mode 100644
index 000000000..74e650b4a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/theme.xpi
Binary files differ
diff --git a/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html
new file mode 100644
index 000000000..42e0e1cd0
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/triggerredirect.html
@@ -0,0 +1,36 @@
+<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN"
+ "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
+
+<html>
+
+<!-- This page will attempt an install and then try to load a new page in the tab -->
+
+<head>
+<title>InstallTrigger tests</title>
+<script type="text/javascript">
+/* globals InstallTrigger */
+function installCallback(url, status) {
+ document.location = "#foo";
+
+ dump("Sending InstallComplete\n");
+ var event = new CustomEvent("InstallComplete");
+ window.dispatchEvent(event);
+}
+
+function startInstall() {
+ InstallTrigger.install({
+ "Unsigned XPI": {
+ URL: "amosigned.xpi",
+ IconURL: "icon.png",
+ toString: function() { return this.URL; }
+ }
+ }, installCallback);
+}
+</script>
+</head>
+<body onload="startInstall()">
+<p>InstallTrigger tests</p>
+<p id="return"></p>
+<p id="status"></p>
+</body>
+</html>
diff --git a/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi b/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
new file mode 100644
index 000000000..51b00475a
--- /dev/null
+++ b/toolkit/mozapps/extensions/test/xpinstall/unsigned.xpi
Binary files differ
diff --git a/toolkit/mozapps/handling/content/dialog.js b/toolkit/mozapps/handling/content/dialog.js
new file mode 100644
index 000000000..98a788201
--- /dev/null
+++ b/toolkit/mozapps/handling/content/dialog.js
@@ -0,0 +1,278 @@
+/* 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 dialog builds its content based on arguments passed into it.
+ * window.arguments[0]:
+ * The title of the dialog.
+ * window.arguments[1]:
+ * The url of the image that appears to the left of the description text
+ * window.arguments[2]:
+ * The text of the description that will appear above the choices the user
+ * can choose from.
+ * window.arguments[3]:
+ * The text of the label directly above the choices the user can choose from.
+ * window.arguments[4]:
+ * This is the text to be placed in the label for the checkbox. If no text is
+ * passed (ie, it's an empty string), the checkbox will be hidden.
+ * window.arguments[5]:
+ * The accesskey for the checkbox
+ * window.arguments[6]:
+ * This is the text that is displayed below the checkbox when it is checked.
+ * window.arguments[7]:
+ * This is the nsIHandlerInfo that gives us all our precious information.
+ * window.arguments[8]:
+ * This is the nsIURI that we are being brought up for in the first place.
+ * window.arguments[9]:
+ * The nsIInterfaceRequestor of the parent window; may be null
+ */
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cr = Components.results;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/SharedPromptUtils.jsm");
+
+
+var dialog = {
+ // Member Variables
+
+ _handlerInfo: null,
+ _URI: null,
+ _itemChoose: null,
+ _okButton: null,
+ _windowCtxt: null,
+ _buttonDisabled: true,
+
+ // Methods
+
+ /**
+ * This function initializes the content of the dialog.
+ */
+ initialize: function initialize()
+ {
+ this._handlerInfo = window.arguments[7].QueryInterface(Ci.nsIHandlerInfo);
+ this._URI = window.arguments[8].QueryInterface(Ci.nsIURI);
+ this._windowCtxt = window.arguments[9];
+ if (this._windowCtxt)
+ this._windowCtxt.QueryInterface(Ci.nsIInterfaceRequestor);
+ this._itemChoose = document.getElementById("item-choose");
+ this._okButton = document.documentElement.getButton("accept");
+
+ var description = {
+ image: document.getElementById("description-image"),
+ text: document.getElementById("description-text")
+ };
+ var options = document.getElementById("item-action-text");
+ var checkbox = {
+ desc: document.getElementById("remember"),
+ text: document.getElementById("remember-text")
+ };
+
+ // Setting values
+ document.title = window.arguments[0];
+ description.image.src = window.arguments[1];
+ description.text.textContent = window.arguments[2];
+ options.value = window.arguments[3];
+ checkbox.desc.label = window.arguments[4];
+ checkbox.desc.accessKey = window.arguments[5];
+ checkbox.text.textContent = window.arguments[6];
+
+ // Hide stuff that needs to be hidden
+ if (!checkbox.desc.label)
+ checkbox.desc.hidden = true;
+
+ // UI is ready, lets populate our list
+ this.populateList();
+
+ this._delayHelper = new EnableDelayHelper({
+ disableDialog: () => {
+ this._buttonDisabled = true;
+ this.updateOKButton();
+ },
+ enableDialog: () => {
+ this._buttonDisabled = false;
+ this.updateOKButton();
+ },
+ focusTarget: window
+ });
+ },
+
+ /**
+ * Populates the list that a user can choose from.
+ */
+ populateList: function populateList()
+ {
+ var items = document.getElementById("items");
+ var possibleHandlers = this._handlerInfo.possibleApplicationHandlers;
+ var preferredHandler = this._handlerInfo.preferredApplicationHandler;
+ var ios = Cc["@mozilla.org/network/io-service;1"].
+ getService(Ci.nsIIOService);
+ for (let i = possibleHandlers.length - 1; i >= 0; --i) {
+ let app = possibleHandlers.queryElementAt(i, Ci.nsIHandlerApp);
+ let elm = document.createElement("richlistitem");
+ elm.setAttribute("type", "handler");
+ elm.setAttribute("name", app.name);
+ elm.obj = app;
+
+ if (app instanceof Ci.nsILocalHandlerApp) {
+ // See if we have an nsILocalHandlerApp and set the icon
+ let uri = ios.newFileURI(app.executable);
+ elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32");
+ }
+ else if (app instanceof Ci.nsIWebHandlerApp) {
+ let uri = ios.newURI(app.uriTemplate, null, null);
+ if (/^https?/.test(uri.scheme)) {
+ // Unfortunately we can't use the favicon service to get the favicon,
+ // because the service looks for a record with the exact URL we give
+ // it, and users won't have such records for URLs they don't visit,
+ // and users won't visit the handler's URL template, they'll only
+ // visit URLs derived from that template (i.e. with %s in the template
+ // replaced by the URL of the content being handled).
+ elm.setAttribute("image", uri.prePath + "/favicon.ico");
+ }
+ elm.setAttribute("description", uri.prePath);
+ }
+ else if (app instanceof Ci.nsIDBusHandlerApp) {
+ elm.setAttribute("description", app.method);
+ }
+ else
+ throw "unknown handler type";
+
+ items.insertBefore(elm, this._itemChoose);
+ if (preferredHandler && app == preferredHandler)
+ this.selectedItem = elm;
+ }
+
+ if (this._handlerInfo.hasDefaultHandler) {
+ let elm = document.createElement("richlistitem");
+ elm.setAttribute("type", "handler");
+ elm.id = "os-default-handler";
+ elm.setAttribute("name", this._handlerInfo.defaultDescription);
+
+ items.insertBefore(elm, items.firstChild);
+ if (this._handlerInfo.preferredAction ==
+ Ci.nsIHandlerInfo.useSystemDefault)
+ this.selectedItem = elm;
+ }
+ items.ensureSelectedElementIsVisible();
+ },
+
+ /**
+ * Brings up a filepicker and allows a user to choose an application.
+ */
+ chooseApplication: function chooseApplication()
+ {
+ var bundle = document.getElementById("base-strings");
+ var title = bundle.getString("choose.application.title");
+
+ var fp = Cc["@mozilla.org/filepicker;1"].createInstance(Ci.nsIFilePicker);
+ fp.init(window, title, Ci.nsIFilePicker.modeOpen);
+ fp.appendFilters(Ci.nsIFilePicker.filterApps);
+
+ if (fp.show() == Ci.nsIFilePicker.returnOK && fp.file) {
+ let uri = Cc["@mozilla.org/network/util;1"].
+ getService(Ci.nsIIOService).
+ newFileURI(fp.file);
+
+ let handlerApp = Cc["@mozilla.org/uriloader/local-handler-app;1"].
+ createInstance(Ci.nsILocalHandlerApp);
+ handlerApp.executable = fp.file;
+
+ // if this application is already in the list, select it and don't add it again
+ let parent = document.getElementById("items");
+ for (let i = 0; i < parent.childNodes.length; ++i) {
+ let elm = parent.childNodes[i];
+ if (elm.obj instanceof Ci.nsILocalHandlerApp && elm.obj.equals(handlerApp)) {
+ parent.selectedItem = elm;
+ parent.ensureSelectedElementIsVisible();
+ return;
+ }
+ }
+
+ let elm = document.createElement("richlistitem");
+ elm.setAttribute("type", "handler");
+ elm.setAttribute("name", fp.file.leafName);
+ elm.setAttribute("image", "moz-icon://" + uri.spec + "?size=32");
+ elm.obj = handlerApp;
+
+ parent.selectedItem = parent.insertBefore(elm, parent.firstChild);
+ parent.ensureSelectedElementIsVisible();
+ }
+ },
+
+ /**
+ * Function called when the OK button is pressed.
+ */
+ onAccept: function onAccept()
+ {
+ var checkbox = document.getElementById("remember");
+ if (!checkbox.hidden) {
+ // We need to make sure that the default is properly set now
+ if (this.selectedItem.obj) {
+ // default OS handler doesn't have this property
+ this._handlerInfo.preferredAction = Ci.nsIHandlerInfo.useHelperApp;
+ this._handlerInfo.preferredApplicationHandler = this.selectedItem.obj;
+ }
+ else
+ this._handlerInfo.preferredAction = Ci.nsIHandlerInfo.useSystemDefault;
+ }
+ this._handlerInfo.alwaysAskBeforeHandling = !checkbox.checked;
+
+ var hs = Cc["@mozilla.org/uriloader/handler-service;1"].
+ getService(Ci.nsIHandlerService);
+ hs.store(this._handlerInfo);
+
+ this._handlerInfo.launchWithURI(this._URI, this._windowCtxt);
+
+ return true;
+ },
+
+ /**
+ * Determines if the OK button should be disabled or not
+ */
+ updateOKButton: function updateOKButton()
+ {
+ this._okButton.disabled = this._itemChoose.selected ||
+ this._buttonDisabled;
+ },
+
+ /**
+ * Updates the UI based on the checkbox being checked or not.
+ */
+ onCheck: function onCheck()
+ {
+ if (document.getElementById("remember").checked)
+ document.getElementById("remember-text").setAttribute("visible", "true");
+ else
+ document.getElementById("remember-text").removeAttribute("visible");
+ },
+
+ /**
+ * Function called when the user double clicks on an item of the list
+ */
+ onDblClick: function onDblClick()
+ {
+ if (this.selectedItem == this._itemChoose)
+ this.chooseApplication();
+ else
+ document.documentElement.acceptDialog();
+ },
+
+ // Getters / Setters
+
+ /**
+ * Returns/sets the selected element in the richlistbox
+ */
+ get selectedItem()
+ {
+ return document.getElementById("items").selectedItem;
+ },
+ set selectedItem(aItem)
+ {
+ return document.getElementById("items").selectedItem = aItem;
+ }
+
+};
diff --git a/toolkit/mozapps/handling/content/dialog.xul b/toolkit/mozapps/handling/content/dialog.xul
new file mode 100644
index 000000000..f73ce6d7f
--- /dev/null
+++ b/toolkit/mozapps/handling/content/dialog.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin/global.css"?>
+<?xml-stylesheet href="chrome://mozapps/content/handling/handler.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/handling/handling.css"?>
+<!-- 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 dialog SYSTEM "chrome://mozapps/locale/handling/handling.dtd">
+
+<dialog id="handling"
+ ondialogaccept="return dialog.onAccept();"
+ onload="dialog.initialize();"
+ style="min-width: &window.emWidth;; min-height: &window.emHeight;;"
+ persist="width height screenX screenY"
+ aria-describedby="description-text"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+ <script src="chrome://mozapps/content/handling/dialog.js" type="application/javascript"/>
+
+ <stringbundleset id="strings">
+ <stringbundle id="base-strings"
+ src="chrome://mozapps/locale/handling/handling.properties"/>
+ </stringbundleset>
+
+ <hbox>
+ <image id="description-image"/>
+ <description id="description-text"/>
+ </hbox>
+
+ <vbox flex="1">
+ <label id="item-action-text" control="items"/>
+ <richlistbox id="items" flex="1"
+ ondblclick="dialog.onDblClick();"
+ onselect="dialog.updateOKButton();">
+ <richlistitem id="item-choose" orient="horizontal" selected="true">
+ <label value="&ChooseOtherApp.description;" flex="1"/>
+ <button oncommand="dialog.chooseApplication();"
+ label="&ChooseApp.label;" accesskey="&ChooseApp.accessKey;"/>
+ </richlistitem>
+ </richlistbox>
+ </vbox>
+
+ <checkbox id="remember" aria-describedby="remember-text" oncommand="dialog.onCheck();"/>
+ <description id="remember-text"/>
+
+ <hbox class="dialog-button-box" pack="end">
+ <button dlgtype="cancel" icon="cancel" class="dialog-button"/>
+ <button dlgtype="accept" label="&accept;" icon="open" class="dialog-button"/>
+ </hbox>
+
+</dialog>
diff --git a/toolkit/mozapps/handling/content/handler.css b/toolkit/mozapps/handling/content/handler.css
new file mode 100644
index 000000000..438ab296d
--- /dev/null
+++ b/toolkit/mozapps/handling/content/handler.css
@@ -0,0 +1,11 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+richlistitem[type="handler"] {
+ -moz-binding: url('chrome://mozapps/content/handling/handler.xml#handler');
+}
+
+#remember-text:not([visible]) {
+ visibility: hidden;
+}
diff --git a/toolkit/mozapps/handling/content/handler.xml b/toolkit/mozapps/handling/content/handler.xml
new file mode 100644
index 000000000..3fd907b45
--- /dev/null
+++ b/toolkit/mozapps/handling/content/handler.xml
@@ -0,0 +1,28 @@
+<?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/. -->
+
+<bindings id="hanlder-bindings"
+ 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">
+
+ <binding id="handler"
+ extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+
+ <content>
+ <xul:vbox pack="center">
+ <xul:image xbl:inherits="src=image" height="32" width="32"/>
+ </xul:vbox>
+ <xul:vbox flex="1">
+ <xul:label class="name" xbl:inherits="value=name"/>
+ <xul:label class="description" xbl:inherits="value=description"/>
+ </xul:vbox>
+ </content>
+ <implementation>
+ <property name="label" onget="return this.getAttribute('name') + ' ' + this.getAttribute('description');"/>
+ </implementation>
+ </binding>
+
+</bindings>
diff --git a/toolkit/mozapps/handling/jar.mn b/toolkit/mozapps/handling/jar.mn
new file mode 100644
index 000000000..d6225ecca
--- /dev/null
+++ b/toolkit/mozapps/handling/jar.mn
@@ -0,0 +1,10 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+ content/mozapps/handling/handler.css (content/handler.css)
+ content/mozapps/handling/handler.xml (content/handler.xml)
+ content/mozapps/handling/dialog.xul (content/dialog.xul)
+ content/mozapps/handling/dialog.js (content/dialog.js)
diff --git a/toolkit/mozapps/handling/moz.build b/toolkit/mozapps/handling/moz.build
new file mode 100644
index 000000000..19730db2f
--- /dev/null
+++ b/toolkit/mozapps/handling/moz.build
@@ -0,0 +1,12 @@
+# -*- 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_COMPONENTS += [
+ 'nsContentDispatchChooser.js',
+ 'nsContentDispatchChooser.manifest',
+]
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/toolkit/mozapps/handling/nsContentDispatchChooser.js b/toolkit/mozapps/handling/nsContentDispatchChooser.js
new file mode 100644
index 000000000..e02545594
--- /dev/null
+++ b/toolkit/mozapps/handling/nsContentDispatchChooser.js
@@ -0,0 +1,85 @@
+/* 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/. */
+
+Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
+
+// Constants
+
+const Cc = Components.classes;
+const Ci = Components.interfaces;
+const Cr = Components.results;
+
+const CONTENT_HANDLING_URL = "chrome://mozapps/content/handling/dialog.xul";
+const STRINGBUNDLE_URL = "chrome://mozapps/locale/handling/handling.properties";
+
+// nsContentDispatchChooser class
+
+function nsContentDispatchChooser()
+{
+}
+
+nsContentDispatchChooser.prototype =
+{
+ classID: Components.ID("e35d5067-95bc-4029-8432-e8f1e431148d"),
+
+ // nsIContentDispatchChooser
+
+ ask: function ask(aHandler, aWindowContext, aURI, aReason)
+ {
+ var window = null;
+ try {
+ if (aWindowContext)
+ window = aWindowContext.getInterface(Ci.nsIDOMWindow);
+ } catch (e) { /* it's OK to not have a window */ }
+
+ var sbs = Cc["@mozilla.org/intl/stringbundle;1"].
+ getService(Ci.nsIStringBundleService);
+ var bundle = sbs.createBundle(STRINGBUNDLE_URL);
+
+ var xai = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo);
+ // TODO when this is hooked up for content, we will need different strings
+ // for most of these
+ var arr = [bundle.GetStringFromName("protocol.title"),
+ "",
+ bundle.GetStringFromName("protocol.description"),
+ bundle.GetStringFromName("protocol.choices.label"),
+ bundle.formatStringFromName("protocol.checkbox.label",
+ [aURI.scheme], 1),
+ bundle.GetStringFromName("protocol.checkbox.accesskey"),
+ bundle.formatStringFromName("protocol.checkbox.extra",
+ [xai.name], 1)];
+
+ var params = Cc["@mozilla.org/array;1"].createInstance(Ci.nsIMutableArray);
+ let SupportsString = Components.Constructor(
+ "@mozilla.org/supports-string;1",
+ "nsISupportsString");
+ for (let text of arr) {
+ let string = new SupportsString;
+ string.data = text;
+ params.appendElement(string, false);
+ }
+ params.appendElement(aHandler, false);
+ params.appendElement(aURI, false);
+ params.appendElement(aWindowContext, false);
+
+ var ww = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+ getService(Ci.nsIWindowWatcher);
+ ww.openWindow(window,
+ CONTENT_HANDLING_URL,
+ null,
+ "chrome,dialog=yes,resizable,centerscreen",
+ params);
+ },
+
+ // nsISupports
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIContentDispatchChooser])
+};
+
+// Module
+
+var components = [nsContentDispatchChooser];
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/mozapps/handling/nsContentDispatchChooser.manifest b/toolkit/mozapps/handling/nsContentDispatchChooser.manifest
new file mode 100644
index 000000000..fcc76a410
--- /dev/null
+++ b/toolkit/mozapps/handling/nsContentDispatchChooser.manifest
@@ -0,0 +1,2 @@
+component {e35d5067-95bc-4029-8432-e8f1e431148d} nsContentDispatchChooser.js
+contract @mozilla.org/content-dispatch-chooser;1 {e35d5067-95bc-4029-8432-e8f1e431148d}
diff --git a/toolkit/mozapps/installer/find-dupes.py b/toolkit/mozapps/installer/find-dupes.py
new file mode 100644
index 000000000..bd0561c97
--- /dev/null
+++ b/toolkit/mozapps/installer/find-dupes.py
@@ -0,0 +1,135 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+import hashlib
+import re
+from mozbuild.preprocessor import Preprocessor
+from mozbuild.util import DefinesAction
+from mozpack.packager.unpack import UnpackFinder
+from mozpack.files import DeflatedFile
+from collections import OrderedDict
+from StringIO import StringIO
+import argparse
+import buildconfig
+
+'''
+Find files duplicated in a given packaged directory, independently of its
+package format.
+'''
+
+
+def normalize_osx_path(p):
+ '''
+ Strips the first 3 elements of an OSX app path
+
+ >>> normalize_osx_path('Nightly.app/foo/bar/baz')
+ 'baz'
+ '''
+ bits = p.split('/')
+ if len(bits) > 3 and bits[0].endswith('.app'):
+ return '/'.join(bits[3:])
+ return p
+
+
+def normalize_l10n_path(p):
+ '''
+ Normalizes localized paths to en-US
+
+ >>> normalize_l10n_path('chrome/es-ES/locale/branding/brand.properties')
+ 'chrome/en-US/locale/branding/brand.properties'
+ >>> normalize_l10n_path('chrome/fr/locale/fr/browser/aboutHome.dtd')
+ 'chrome/en-US/locale/en-US/browser/aboutHome.dtd'
+ '''
+ # Keep a trailing slash here! e.g. locales like 'br' can transform
+ # 'chrome/br/locale/branding/' into 'chrome/en-US/locale/en-USanding/'
+ p = re.sub(r'chrome/(\S+)/locale/\1/',
+ 'chrome/en-US/locale/en-US/',
+ p)
+ p = re.sub(r'chrome/(\S+)/locale/',
+ 'chrome/en-US/locale/',
+ p)
+ return p
+
+
+def normalize_path(p):
+ return normalize_osx_path(normalize_l10n_path(p))
+
+
+def find_dupes(source, allowed_dupes, bail=True):
+ allowed_dupes = set(allowed_dupes)
+ md5s = OrderedDict()
+ for p, f in UnpackFinder(source):
+ content = f.open().read()
+ m = hashlib.md5(content).digest()
+ if m not in md5s:
+ if isinstance(f, DeflatedFile):
+ compressed = f.file.compressed_size
+ else:
+ compressed = len(content)
+ md5s[m] = (len(content), compressed, [])
+ md5s[m][2].append(p)
+ total = 0
+ total_compressed = 0
+ num_dupes = 0
+ unexpected_dupes = []
+ for m, (size, compressed, paths) in sorted(md5s.iteritems(),
+ key=lambda x: x[1][1]):
+ if len(paths) > 1:
+ print 'Duplicates %d bytes%s%s:' % (size,
+ ' (%d compressed)' % compressed if compressed != size else '',
+ ' (%d times)' % (len(paths) - 1) if len(paths) > 2 else '')
+ print ''.join(' %s\n' % p for p in paths)
+ total += (len(paths) - 1) * size
+ total_compressed += (len(paths) - 1) * compressed
+ num_dupes += 1
+
+ unexpected_dupes.extend([p for p in paths if normalize_path(p) not in allowed_dupes])
+
+ if num_dupes:
+ print "WARNING: Found %d duplicated files taking %d bytes (%s)" % \
+ (num_dupes, total,
+ '%d compressed' % total_compressed if total_compressed != total
+ else 'uncompressed')
+
+ if unexpected_dupes:
+ errortype = "ERROR" if bail else "WARNING"
+ print "%s: The following duplicated files are not allowed:" % errortype
+ print "\n".join(unexpected_dupes)
+ if bail:
+ sys.exit(1)
+
+
+def main():
+ parser = argparse.ArgumentParser(description='Find duplicate files in directory.')
+ parser.add_argument('--warning', '-w', action='store_true',
+ help='Only warn about duplicates, do not exit with an error')
+ parser.add_argument('--file', '-f', action='append', dest='dupes_files', default=[],
+ help='Add exceptions to the duplicate list from this file')
+ parser.add_argument('-D', action=DefinesAction)
+ parser.add_argument('-U', action='append', default=[])
+ parser.add_argument('directory',
+ help='The directory to check for duplicates in')
+
+ args = parser.parse_args()
+
+ allowed_dupes = []
+ for filename in args.dupes_files:
+ pp = Preprocessor()
+ pp.context.update(buildconfig.defines)
+ if args.D:
+ pp.context.update(args.D)
+ for undefine in args.U:
+ if undefine in pp.context:
+ del pp.context[undefine]
+ pp.out = StringIO()
+ pp.do_filter('substitution')
+ pp.do_include(filename)
+ allowed_dupes.extend([line.partition('#')[0].rstrip()
+ for line in pp.out.getvalue().splitlines()])
+
+ find_dupes(args.directory, bail=not args.warning, allowed_dupes=allowed_dupes)
+
+if __name__ == "__main__":
+ main()
diff --git a/toolkit/mozapps/installer/informulate.py b/toolkit/mozapps/installer/informulate.py
new file mode 100644
index 000000000..6af43ac68
--- /dev/null
+++ b/toolkit/mozapps/installer/informulate.py
@@ -0,0 +1,46 @@
+#!/usr/bin/env 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/.
+
+import sys
+import json
+import buildconfig
+
+
+def parse_cmdline(args):
+ """Take a list of strings in the format K=V and turn them into a python
+ dictionary"""
+ contents = {}
+ for arg in args:
+ key, s, value = arg.partition("=")
+ if s == '':
+ print "ERROR: Malformed command line key value pairing (%s)" % arg
+ exit(1)
+ contents[key.lower()] = value
+ return contents
+
+
+def main():
+ if len(sys.argv) < 2:
+ print "ERROR: You must specify an output file"
+ exit(1)
+
+ all_key_value_pairs = {}
+ important_substitutions = [
+ 'target_alias', 'target_cpu', 'target_os', 'target_vendor',
+ 'host_alias', 'host_cpu', 'host_os', 'host_vendor',
+ 'MOZ_UPDATE_CHANNEL', 'MOZ_APP_VENDOR', 'MOZ_APP_NAME',
+ 'MOZ_APP_VERSION', 'MOZ_APP_MAXVERSION', 'MOZ_APP_ID',
+ 'CC', 'CXX', 'LD', 'AS']
+
+ all_key_value_pairs = dict([(x.lower(), buildconfig.substs[x]) for x in important_substitutions])
+ all_key_value_pairs.update(parse_cmdline(sys.argv[2:]))
+
+ with open(sys.argv[1], "w+") as f:
+ json.dump(all_key_value_pairs, f, indent=2, sort_keys=True)
+ f.write('\n')
+
+
+if __name__=="__main__":
+ main()
diff --git a/toolkit/mozapps/installer/js-compare-ast.js b/toolkit/mozapps/installer/js-compare-ast.js
new file mode 100644
index 000000000..5b66ace37
--- /dev/null
+++ b/toolkit/mozapps/installer/js-compare-ast.js
@@ -0,0 +1,28 @@
+/* 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 script compares the AST of two JavaScript files passed as arguments.
+ * The script exits with a 0 status code if both files parse properly and the
+ * ASTs of both files are identical modulo location differences. The script
+ * exits with status code 1 if any of these conditions don't hold.
+ *
+ * This script is used as part of packaging to verify minified JavaScript files
+ * are identical to their original files.
+ */
+
+"use strict";
+
+function ast(filename) {
+ return JSON.stringify(Reflect.parse(snarf(filename), {loc: 0}));
+}
+
+if (scriptArgs.length !== 2) {
+ throw "usage: js js-compare-ast.js FILE1.js FILE2.js";
+}
+
+var ast0 = ast(scriptArgs[0]);
+var ast1 = ast(scriptArgs[1]);
+
+quit(ast0 == ast1 ? 0 : 1);
diff --git a/toolkit/mozapps/installer/l10n-repack.py b/toolkit/mozapps/installer/l10n-repack.py
new file mode 100644
index 000000000..783c00b71
--- /dev/null
+++ b/toolkit/mozapps/installer/l10n-repack.py
@@ -0,0 +1,60 @@
+# 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/.
+
+'''
+Replace localized parts of a packaged directory with data from a langpack
+directory.
+'''
+
+from mozpack.packager import l10n
+from argparse import ArgumentParser
+import buildconfig
+
+# Set of files or directories not listed in a chrome.manifest but that are
+# localized.
+NON_CHROME = set([
+ '**/crashreporter*.ini',
+ 'searchplugins',
+ 'dictionaries',
+ 'hyphenation',
+ 'defaults/profile',
+ 'defaults/pref*/*-l10n.js',
+ 'update.locale',
+ 'updater.ini',
+ 'extensions/langpack-*@*',
+ 'distribution/extensions/langpack-*@*',
+ 'chrome/**/searchplugins/*.xml',
+])
+
+
+def valid_extra_l10n(arg):
+ if '=' not in arg:
+ raise ValueError('Invalid value')
+ return tuple(arg.split('=', 1))
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument('build',
+ help='Directory containing the build to repack')
+ parser.add_argument('l10n',
+ help='Directory containing the staged langpack')
+ parser.add_argument('extra_l10n', nargs='*', metavar='BASE=PATH',
+ type=valid_extra_l10n,
+ help='Extra directories with staged localized files '
+ 'to be considered under the given base in the '
+ 'repacked build')
+ parser.add_argument('--non-resource', nargs='+', metavar='PATTERN',
+ default=[],
+ help='Extra files not to be considered as resources')
+ args = parser.parse_args()
+
+ buildconfig.substs['USE_ELF_HACK'] = False
+ buildconfig.substs['PKG_SKIP_STRIP'] = True
+ l10n.repack(args.build, args.l10n, extra_l10n=dict(args.extra_l10n),
+ non_resources=args.non_resource, non_chrome=NON_CHROME)
+
+
+if __name__ == "__main__":
+ main()
diff --git a/toolkit/mozapps/installer/linux/rpm/mozilla.desktop b/toolkit/mozapps/installer/linux/rpm/mozilla.desktop
new file mode 100644
index 000000000..f9f3bdc34
--- /dev/null
+++ b/toolkit/mozapps/installer/linux/rpm/mozilla.desktop
@@ -0,0 +1,21 @@
+#filter substitution
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[Desktop Entry]
+Version=1.0
+Name=@MOZ_APP_DISPLAYNAME@
+GenericName=Web Browser
+Comment=Your web, the way you like it
+Exec=@MOZ_APP_NAME@
+Icon=@MOZ_APP_NAME@
+Terminal=false
+Type=Application
+StartupWMClass=@MOZ_APP_NAME@-bin
+MimeType=text/html;text/xml;application/xhtml+xml;application/vnd.mozilla.xul+xml;text/mml;x-scheme-handler/http;x-scheme-handler/https;
+StartupNotify=true
+X-MultipleArgs=false
+X-Desktop-File-Install-Version=0.16
+Categories=Network;WebBrowser;
+Encoding=UTF-8
diff --git a/toolkit/mozapps/installer/linux/rpm/mozilla.spec b/toolkit/mozapps/installer/linux/rpm/mozilla.spec
new file mode 100644
index 000000000..90d132558
--- /dev/null
+++ b/toolkit/mozapps/installer/linux/rpm/mozilla.spec
@@ -0,0 +1,134 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+%global __jar_repack %{nil}
+
+#Use a consistent string to refer to the package by
+%define pr_name "%{moz_app_displayname} %{moz_app_version}"
+
+Name: %{moz_app_name}
+Version: %{moz_numeric_app_version}
+Release: %{?moz_rpm_release:%{moz_rpm_release}}%{?buildid:.%{buildid}}
+Summary: %{pr_name}
+Group: Applications/Internet
+License: MPL 2
+Vendor: Mozilla
+URL: http://www.mozilla.org/projects/firefox/
+Source0: %{name}.desktop
+BuildRoot: %{_tmppath}/%{name}-%{version}-%{release}-root-%(%{__id_u} -n)
+
+#AutoProv: no
+
+BuildRequires: desktop-file-utils
+
+
+%description
+%{pr_name}. This package was built from
+%{moz_source_repo}/rev/%{moz_source_stamp}
+
+#We only want a subpackage for the SDK if the required
+#files were generated. Like the tests subpackage, we
+#probably only need to conditionaly define the %files
+#section.
+%if %{?createdevel:1}
+%package devel
+Summary: %{pr_name} SDK
+Group: Development/Libraries
+requires: %{name} = %{version}-%{release}
+%description devel
+%{pr_name} SDK libraries, headers and interface descriptions
+%endif
+
+%if %{?createtests:1}
+%package tests
+Summary: %{pr_name} tests
+Group: Developement/Libraries
+requires: %{name} = %{version}-%{release}
+%description tests
+%{pr_name} test harness files and test cases
+%endif
+
+%prep
+echo No-op prep
+
+
+%build
+echo No-op build
+
+
+%install
+rm -rf $RPM_BUILD_ROOT
+make install DESTDIR=$RPM_BUILD_ROOT
+desktop-file-validate %{SOURCE0}
+desktop-file-install --vendor mozilla \
+ --dir $RPM_BUILD_ROOT%{_datadir}/applications \
+ %{SOURCE0}
+#In order to make branding work in a generic way, We find
+#all the icons that are likely to be used for desktop files
+#and install them appropriately
+find %{moz_branding_directory} -name "default*.png" | tee icons.list
+for i in $(cat icons.list) ; do
+ size=$(echo $i | sed "s/.*default\([0-9]*\).png$/\1/")
+ icondir=$RPM_BUILD_ROOT/%{_datadir}/icons/hicolor/${size}x${size}/apps/
+ mkdir -p $icondir
+ cp -a $i ${icondir}%{name}.png
+done
+rm icons.list #cleanup
+
+%if %{?createtests:1}
+#wastefully creates a zip file, but ensures that we stage all test suites
+make package-tests
+testdir=$RPM_BUILD_ROOT/%{_datadir}/%{_testsinstalldir}/tests
+mkdir -p $testdir
+cp -a dist/test-stage/* $testdir/
+%endif
+
+%clean
+rm -rf $RPM_BUILD_ROOT
+
+
+%post
+#this is needed to get gnome-panel to update the icons
+update-desktop-database &> /dev/null || :
+touch --no-create %{_datadir}/icons/hicolor || :
+if [ -x %{_bindir}/gtk-update-icon-cache ] ; then
+ %{_bindir}/gtk-update-icon-cache --quiet ${_datadir}/icons/hicolor &> /dev/null || :
+fi
+
+
+%postun
+#this is needed to get gnome-panel to update the icons
+update-desktop-database &> /dev/null || :
+touch --no-create %{_datadir}/icons/hicolor || :
+if [ -x %{_bindir}/gtk-update-icon-cache ] ; then
+ %{_bindir}/gtk-update-icon-cache --quiet ${_datadir}/icons/hicolor &> /dev/null || :
+fi
+
+
+%files
+%defattr(-,root,root,-)
+%{_installdir}
+%{_bindir}
+%{_datadir}/applications/
+%{_datadir}/icons/
+%doc
+
+
+%if %{?createdevel:1}
+%files devel
+%defattr(-,root,root,-)
+%{_includedir}
+%{_sdkdir}
+%{_idldir}
+%endif
+
+
+%if %{?createtests:1}
+%files tests
+%{_datadir}/%{_testsinstalldir}/tests/
+%endif
+
+#%changelog
+#* %{name} %{version} %{moz_rpm_release}
+#- Please see %{moz_source_repo}/shortlog/%{moz_source_stamp}
diff --git a/toolkit/mozapps/installer/make-eme.mk b/toolkit/mozapps/installer/make-eme.mk
new file mode 100644
index 000000000..739703cfd
--- /dev/null
+++ b/toolkit/mozapps/installer/make-eme.mk
@@ -0,0 +1,16 @@
+# 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 $(MOZILLA_DIR)/toolkit/mozapps/installer/signing.mk
+
+ifdef MOZ_SIGN_CMD
+ ifeq (,$(filter-out WINNT Darwin,$(OS_ARCH)))
+ # The first argument to this macro is the directory where the
+ # plugin-container binary exists, and the second is where voucher.bin will
+ # be generated. If the second argument is not specified, it defaults to the
+ # same as the first.
+ MAKE_SIGN_EME_VOUCHER = $(PYTHON) $(MOZILLA_DIR)/python/eme/gen-eme-voucher.py -input $(1)/$(MOZ_CHILD_PROCESS_NAME) -output $(or $(2),$(1))/voucher.bin && \
+ $(MOZ_SIGN_CMD) -f emevoucher "$(or $(2),$(1))/voucher.bin"
+ endif
+endif
diff --git a/toolkit/mozapps/installer/package-name.mk b/toolkit/mozapps/installer/package-name.mk
new file mode 100644
index 000000000..b1ac9a588
--- /dev/null
+++ b/toolkit/mozapps/installer/package-name.mk
@@ -0,0 +1,168 @@
+# 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/.
+
+# assemble package names, see convention at
+# http://developer.mozilla.org/index.php?title=En/Package_Filename_Convention
+# for (at least Firefox) releases we use a different format with directories,
+# e.g. win32/de/Firefox Setup 3.0.1.exe
+# the latter format is triggered with MOZ_PKG_PRETTYNAMES=1
+
+ifndef PACKAGE_NAME_MK_INCLUDED
+PACKAGE_NAME_MK_INCLUDED := 1
+
+ifndef MOZ_PKG_VERSION
+MOZ_PKG_VERSION = $(MOZ_APP_VERSION)
+endif
+
+ifndef MOZ_PKG_PLATFORM
+MOZ_PKG_PLATFORM := $(TARGET_OS)-$(TARGET_CPU)
+
+ifeq ($(MOZ_BUILD_APP),mobile/android)
+MOZ_PKG_PLATFORM := android-$(TARGET_CPU)
+endif
+
+# TARGET_OS/TARGET_CPU may be unintuitive, so we hardcode some special formats
+ifeq ($(OS_ARCH),WINNT)
+ifeq ($(TARGET_CPU),x86_64)
+MOZ_PKG_PLATFORM := win64
+else
+MOZ_PKG_PLATFORM := win32
+endif
+endif
+ifeq ($(OS_ARCH),Darwin)
+ifdef UNIVERSAL_BINARY
+MOZ_PKG_PLATFORM := mac
+else
+ifeq ($(TARGET_CPU),x86_64)
+MOZ_PKG_PLATFORM := mac64
+else
+MOZ_PKG_PLATFORM := mac
+endif
+endif
+endif
+ifeq ($(TARGET_OS),linux-gnu)
+MOZ_PKG_PLATFORM := linux-$(TARGET_CPU)
+endif
+endif #MOZ_PKG_PLATFORM
+
+ifdef MOZ_PKG_SPECIAL
+MOZ_PKG_PLATFORM := $(MOZ_PKG_PLATFORM)-$(MOZ_PKG_SPECIAL)
+endif
+
+MOZ_PKG_DIR = $(MOZ_APP_NAME)
+
+ifndef MOZ_PKG_PRETTYNAMES # standard package names
+
+ifndef MOZ_PKG_APPNAME
+MOZ_PKG_APPNAME = $(MOZ_APP_NAME)
+endif
+
+ifdef MOZ_SIMPLE_PACKAGE_NAME
+PKG_BASENAME := $(MOZ_SIMPLE_PACKAGE_NAME)
+else
+PKG_BASENAME = $(MOZ_PKG_APPNAME)-$(MOZ_PKG_VERSION).$(AB_CD).$(MOZ_PKG_PLATFORM)
+endif
+PKG_PATH =
+SDK_PATH =
+PKG_INST_BASENAME = $(PKG_BASENAME).installer
+PKG_STUB_BASENAME = $(PKG_BASENAME).installer-stub
+PKG_INST_PATH = install/sea/
+PKG_UPDATE_BASENAME = $(PKG_BASENAME)
+CHECKSUMS_FILE_BASENAME = $(PKG_BASENAME)
+MOZ_INFO_BASENAME = $(PKG_BASENAME)
+PKG_UPDATE_PATH = update/
+COMPLETE_MAR = $(PKG_UPDATE_PATH)$(PKG_UPDATE_BASENAME).complete.mar
+# PARTIAL_MAR needs to be processed by $(wildcard) before you use it.
+PARTIAL_MAR = $(PKG_UPDATE_PATH)$(PKG_UPDATE_BASENAME).partial.*.mar
+PKG_LANGPACK_BASENAME = $(MOZ_PKG_APPNAME)-$(MOZ_PKG_VERSION).$(AB_CD).langpack
+PKG_LANGPACK_PATH = $(MOZ_PKG_PLATFORM)/xpi/
+LANGPACK = $(PKG_LANGPACK_PATH)$(PKG_LANGPACK_BASENAME).xpi
+PKG_SRCPACK_BASENAME = $(MOZ_PKG_APPNAME)-$(MOZ_PKG_VERSION).source
+PKG_BUNDLE_BASENAME = $(MOZ_PKG_APPNAME)-$(MOZ_PKG_VERSION)
+PKG_SRCPACK_PATH =
+
+else # "pretty" release package names
+
+ifndef MOZ_PKG_APPNAME
+MOZ_PKG_APPNAME = $(MOZ_APP_DISPLAYNAME)
+endif
+MOZ_PKG_APPNAME_LC = $(shell echo $(MOZ_PKG_APPNAME) | tr '[A-Z]' '[a-z]')
+
+ifndef MOZ_PKG_LONGVERSION
+MOZ_PKG_LONGVERSION = $(MOZ_PKG_VERSION)
+endif
+
+ifeq (,$(filter-out Darwin, $(OS_ARCH))) # Mac
+PKG_BASENAME = $(MOZ_PKG_APPNAME) $(MOZ_PKG_LONGVERSION)
+PKG_INST_BASENAME = $(MOZ_PKG_APPNAME) Setup $(MOZ_PKG_LONGVERSION)
+else
+ifeq (,$(filter-out WINNT, $(OS_ARCH))) # Windows
+PKG_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+PKG_INST_BASENAME = $(MOZ_PKG_APPNAME) Setup $(MOZ_PKG_LONGVERSION)
+PKG_STUB_BASENAME = $(MOZ_PKG_APPNAME) Setup Stub $(MOZ_PKG_LONGVERSION)
+else # unix (actually, not Windows, Mac or OS/2)
+PKG_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+PKG_INST_BASENAME = $(MOZ_PKG_APPNAME_LC)-setup-$(MOZ_PKG_VERSION)
+endif
+endif
+PKG_PATH = $(MOZ_PKG_PLATFORM)/$(AB_CD)/
+SDK_PATH = $(PKG_PATH)/sdk/
+CHECKSUMS_FILE_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+MOZ_INFO_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+PKG_INST_PATH = $(PKG_PATH)
+PKG_UPDATE_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+PKG_UPDATE_PATH = update/$(PKG_PATH)
+COMPLETE_MAR = $(PKG_UPDATE_PATH)$(PKG_UPDATE_BASENAME).complete.mar
+# PARTIAL_MAR needs to be processed by $(wildcard) before you use it.
+PARTIAL_MAR = $(PKG_UPDATE_PATH)$(PKG_UPDATE_BASENAME).partial.*.mar
+PKG_LANGPACK_BASENAME = $(AB_CD)
+PKG_LANGPACK_PATH = $(MOZ_PKG_PLATFORM)/xpi/
+LANGPACK = $(PKG_LANGPACK_PATH)$(PKG_LANGPACK_BASENAME).xpi
+PKG_SRCPACK_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION).source
+PKG_BUNDLE_BASENAME = $(MOZ_PKG_APPNAME_LC)-$(MOZ_PKG_VERSION)
+PKG_SRCPACK_PATH = source/
+
+endif # MOZ_PKG_PRETTYNAMES
+
+# Symbol package naming
+SYMBOL_FULL_ARCHIVE_BASENAME = $(PKG_BASENAME).crashreporter-symbols-full
+SYMBOL_ARCHIVE_BASENAME = $(PKG_BASENAME).crashreporter-symbols
+
+# Code coverage package naming
+CODE_COVERAGE_ARCHIVE_BASENAME = $(PKG_BASENAME).code-coverage-gcno
+
+# Mozharness naming
+MOZHARNESS_PACKAGE = mozharness.zip
+
+# Test package naming
+TEST_PACKAGE = $(PKG_BASENAME).common.tests.zip
+CPP_TEST_PACKAGE = $(PKG_BASENAME).cppunittest.tests.zip
+XPC_TEST_PACKAGE = $(PKG_BASENAME).xpcshell.tests.zip
+MOCHITEST_PACKAGE = $(PKG_BASENAME).mochitest.tests.zip
+REFTEST_PACKAGE = $(PKG_BASENAME).reftest.tests.zip
+WP_TEST_PACKAGE = $(PKG_BASENAME).web-platform.tests.zip
+TALOS_PACKAGE = $(PKG_BASENAME).talos.tests.zip
+GTEST_PACKAGE = $(PKG_BASENAME).gtest.tests.zip
+
+ifneq (,$(wildcard $(DIST)/bin/application.ini))
+BUILDID = $(shell $(PYTHON) $(MOZILLA_DIR)/config/printconfigsetting.py $(DIST)/bin/application.ini App BuildID)
+else
+BUILDID = $(shell $(PYTHON) $(MOZILLA_DIR)/config/printconfigsetting.py $(DIST)/bin/platform.ini Build BuildID)
+endif
+
+MOZ_SOURCESTAMP_FILE = $(DIST)/$(PKG_PATH)/$(MOZ_INFO_BASENAME).txt
+MOZ_BUILDINFO_FILE = $(DIST)/$(PKG_PATH)/$(MOZ_INFO_BASENAME).json
+MOZ_BUILDID_INFO_TXT_FILE = $(DIST)/$(PKG_PATH)/$(MOZ_INFO_BASENAME)_info.txt
+MOZ_MOZINFO_FILE = $(DIST)/$(PKG_PATH)/$(MOZ_INFO_BASENAME).mozinfo.json
+MOZ_TEST_PACKAGES_FILE = $(DIST)/$(PKG_PATH)/$(PKG_BASENAME).test_packages.json
+
+# JavaScript Shell
+ifdef MOZ_SIMPLE_PACKAGE_NAME
+JSSHELL_NAME := $(MOZ_SIMPLE_PACKAGE_NAME).jsshell.zip
+else
+JSSHELL_NAME = jsshell-$(MOZ_PKG_PLATFORM).zip
+endif
+PKG_JSSHELL = $(DIST)/$(JSSHELL_NAME)
+
+endif # PACKAGE_NAME_MK_INCLUDED
diff --git a/toolkit/mozapps/installer/packager.mk b/toolkit/mozapps/installer/packager.mk
new file mode 100644
index 000000000..80e87a1ec
--- /dev/null
+++ b/toolkit/mozapps/installer/packager.mk
@@ -0,0 +1,250 @@
+# 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 $(MOZILLA_DIR)/toolkit/mozapps/installer/package-name.mk
+include $(MOZILLA_DIR)/toolkit/mozapps/installer/upload-files.mk
+include $(MOZILLA_DIR)/toolkit/mozapps/installer/make-eme.mk
+
+# This is how we create the binary packages we release to the public.
+
+# browser/locales/Makefile uses this makefile for its variable defs, but
+# doesn't want the libs:: rule.
+ifndef PACKAGER_NO_LIBS
+libs:: make-package
+endif
+
+installer-stage: prepare-package
+ifndef MOZ_PKG_MANIFEST
+ $(error MOZ_PKG_MANIFEST unspecified!)
+endif
+ @rm -rf $(DEPTH)/installer-stage $(DIST)/xpt
+ @echo 'Staging installer files...'
+ @$(NSINSTALL) -D $(DEPTH)/installer-stage/core
+ @cp -av $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/. $(DEPTH)/installer-stage/core
+ifdef MOZ_SIGN_PREPARED_PACKAGE_CMD
+# The && true is necessary to make sure Pymake spins a shell
+ $(MOZ_SIGN_PREPARED_PACKAGE_CMD) $(DEPTH)/installer-stage && true
+endif
+ $(call MAKE_SIGN_EME_VOUCHER,$(DEPTH)/installer-stage/core)
+ @(cd $(DEPTH)/installer-stage/core && $(CREATE_PRECOMPLETE_CMD))
+
+ifeq (gonk,$(MOZ_WIDGET_TOOLKIT))
+ELF_HACK_FLAGS = --fill
+endif
+export USE_ELF_HACK ELF_HACK_FLAGS
+
+# Override the value of OMNIJAR_NAME from config.status with the value
+# set earlier in this file.
+
+stage-package: $(MOZ_PKG_MANIFEST) $(MOZ_PKG_MANIFEST_DEPS)
+ OMNIJAR_NAME=$(OMNIJAR_NAME) \
+ NO_PKG_FILES="$(NO_PKG_FILES)" \
+ $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/packager.py $(DEFINES) $(ACDEFINES) \
+ --format $(MOZ_PACKAGER_FORMAT) \
+ $(addprefix --removals ,$(MOZ_PKG_REMOVALS)) \
+ $(if $(filter-out 0,$(MOZ_PKG_FATAL_WARNINGS)),,--ignore-errors) \
+ $(if $(MOZ_PACKAGER_MINIFY),--minify) \
+ $(if $(MOZ_PACKAGER_MINIFY_JS),--minify-js \
+ $(addprefix --js-binary ,$(JS_BINARY)) \
+ ) \
+ $(if $(JARLOG_DIR),$(addprefix --jarlog ,$(wildcard $(JARLOG_FILE_AB_CD)))) \
+ $(if $(OPTIMIZEJARS),--optimizejars) \
+ $(if $(DISABLE_JAR_COMPRESSION),--disable-compression) \
+ $(addprefix --unify ,$(UNIFY_DIST)) \
+ $(MOZ_PKG_MANIFEST) $(DIST) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(if $(MOZ_PKG_MANIFEST),,$(_BINPATH)) \
+ $(if $(filter omni,$(MOZ_PACKAGER_FORMAT)),$(if $(NON_OMNIJAR_FILES),--non-resource $(NON_OMNIJAR_FILES)))
+ $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/find-dupes.py $(DEFINES) $(ACDEFINES) $(MOZ_PKG_DUPEFLAGS) $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)
+ifndef MOZ_THUNDERBIRD
+ # Package mozharness
+ $(call py_action,test_archive, \
+ mozharness \
+ $(ABS_DIST)/$(PKG_PATH)$(MOZHARNESS_PACKAGE))
+endif # MOZ_THUNDERBIRD
+ifdef MOZ_PACKAGE_JSSHELL
+ # Package JavaScript Shell
+ @echo 'Packaging JavaScript Shell...'
+ $(RM) $(PKG_JSSHELL)
+ $(MAKE_JSSHELL)
+endif # MOZ_PACKAGE_JSSHELL
+ifdef MOZ_ARTIFACT_BUILD_SYMBOLS
+ @echo 'Packaging existing crashreporter symbols from artifact build...'
+ $(NSINSTALL) -D $(DIST)/$(PKG_PATH)
+ cd $(DIST)/crashreporter-symbols && \
+ zip -r5D '../$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip' . -i '*.sym' -i '*.txt'
+endif # MOZ_ARTIFACT_BUILD_SYMBOLS
+ifdef MOZ_CODE_COVERAGE
+ # Package code coverage gcno tree
+ @echo 'Packaging code coverage data...'
+ $(RM) $(CODE_COVERAGE_ARCHIVE_BASENAME).zip
+ $(PYTHON) -mmozbuild.codecoverage.packager \
+ --output-file='$(DIST)/$(PKG_PATH)$(CODE_COVERAGE_ARCHIVE_BASENAME).zip'
+endif
+ifeq (Darwin, $(OS_ARCH))
+ifdef MOZ_ASAN
+ @echo "Rewriting ASan runtime dylib paths for all binaries in $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH) ..."
+ $(PYTHON) $(MOZILLA_DIR)/build/unix/rewrite_asan_dylib.py $(DIST)/$(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)
+endif # MOZ_ASAN
+endif # Darwin
+
+prepare-package: stage-package
+
+make-package-internal: prepare-package make-sourcestamp-file make-buildinfo-file make-mozinfo-file
+ @echo 'Compressing...'
+ cd $(DIST) && $(MAKE_PACKAGE)
+
+make-package: FORCE
+ $(MAKE) make-package-internal
+ $(TOUCH) $@
+
+GARBAGE += make-package
+
+make-sourcestamp-file::
+ $(NSINSTALL) -D $(DIST)/$(PKG_PATH)
+ @echo '$(BUILDID)' > $(MOZ_SOURCESTAMP_FILE)
+ifdef MOZ_INCLUDE_SOURCE_INFO
+ @awk '$$2 == "MOZ_SOURCE_URL" {print $$3}' $(DEPTH)/source-repo.h >> $(MOZ_SOURCESTAMP_FILE)
+endif
+
+.PHONY: make-buildinfo-file
+make-buildinfo-file:
+ $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/informulate.py \
+ $(MOZ_BUILDINFO_FILE) \
+ BUILDID=$(BUILDID) \
+ $(addprefix MOZ_SOURCE_REPO=,MOZ_SOURCE_REPO=$(shell awk '$$2 == "MOZ_SOURCE_REPO" {print $$3}' $(DEPTH)/source-repo.h)) \
+ MOZ_SOURCE_STAMP=$(shell awk '$$2 == "MOZ_SOURCE_STAMP" {print $$3}' $(DEPTH)/source-repo.h) \
+ MOZ_PKG_PLATFORM=$(MOZ_PKG_PLATFORM)
+ echo "buildID=$(BUILDID)" > $(MOZ_BUILDID_INFO_TXT_FILE)
+
+.PHONY: make-mozinfo-file
+make-mozinfo-file:
+ cp $(DEPTH)/mozinfo.json $(MOZ_MOZINFO_FILE)
+
+# The install target will install the application to prefix/lib/appname-version
+# In addition if INSTALL_SDK is set, it will install the development headers,
+# libraries, and IDL files as follows:
+# dist/include -> prefix/include/appname-version
+# dist/idl -> prefix/share/idl/appname-version
+# dist/sdk/lib -> prefix/lib/appname-devel-version/lib
+# prefix/lib/appname-devel-version/* symlinks to the above directories
+install:: prepare-package
+ifeq ($(OS_ARCH),WINNT)
+ $(error "make install" is not supported on this platform. Use "make package" instead.)
+endif
+ifeq (bundle,$(MOZ_FS_LAYOUT))
+ $(error "make install" is not supported on this platform. Use "make package" instead.)
+endif
+ $(NSINSTALL) -D $(DESTDIR)$(installdir)
+ (cd $(DIST)/$(MOZ_PKG_DIR) && $(TAR) --exclude=precomplete $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DESTDIR)$(installdir) && tar -xf -)
+ $(NSINSTALL) -D $(DESTDIR)$(bindir)
+ $(RM) -f $(DESTDIR)$(bindir)/$(MOZ_APP_NAME)
+ ln -s $(installdir)/$(MOZ_APP_NAME) $(DESTDIR)$(bindir)
+ifdef INSTALL_SDK # Here comes the hard part
+ $(NSINSTALL) -D $(DESTDIR)$(includedir)
+ (cd $(DIST)/include && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DESTDIR)$(includedir) && tar -xf -)
+ $(NSINSTALL) -D $(DESTDIR)$(idldir)
+ (cd $(DIST)/idl && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DESTDIR)$(idldir) && tar -xf -)
+# SDK directory is the libs + a bunch of symlinks
+ $(NSINSTALL) -D $(DESTDIR)$(sdkdir)/sdk/lib
+ $(NSINSTALL) -D $(DESTDIR)$(sdkdir)/sdk/bin
+ if test -f $(DIST)/include/xpcom-config.h; then \
+ $(SYSINSTALL) $(IFLAGS1) $(DIST)/include/xpcom-config.h $(DESTDIR)$(sdkdir); \
+ fi
+ find $(DIST)/sdk -name '*.pyc' | xargs rm -f
+ (cd $(DIST)/sdk/lib && $(TAR) $(TAR_CREATE_FLAGS) - .) | (cd $(DESTDIR)$(sdkdir)/sdk/lib && tar -xf -)
+ (cd $(DIST)/sdk/bin && $(TAR) $(TAR_CREATE_FLAGS) - .) | (cd $(DESTDIR)$(sdkdir)/sdk/bin && tar -xf -)
+ $(RM) -f $(DESTDIR)$(sdkdir)/lib $(DESTDIR)$(sdkdir)/bin $(DESTDIR)$(sdkdir)/include $(DESTDIR)$(sdkdir)/include $(DESTDIR)$(sdkdir)/sdk/idl $(DESTDIR)$(sdkdir)/idl
+ ln -s $(sdkdir)/sdk/lib $(DESTDIR)$(sdkdir)/lib
+ ln -s $(installdir) $(DESTDIR)$(sdkdir)/bin
+ ln -s $(includedir) $(DESTDIR)$(sdkdir)/include
+ ln -s $(idldir) $(DESTDIR)$(sdkdir)/idl
+endif # INSTALL_SDK
+
+make-sdk:
+ifndef SDK_UNIFY
+ $(MAKE) stage-package UNIVERSAL_BINARY= STAGE_SDK=1 MOZ_PKG_DIR=sdk-stage
+endif
+ @echo 'Packaging SDK...'
+ $(RM) -rf $(DIST)/$(MOZ_APP_NAME)-sdk
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/bin
+ifdef SDK_UNIFY
+ (cd $(UNIFY_DIST)/sdk-stage && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/bin && tar -xf -)
+else
+ (cd $(DIST)/sdk-stage && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/bin && tar -xf -)
+endif
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/host/bin
+ (cd $(DIST)/host/bin && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/host/bin && tar -xf -)
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/sdk
+ find $(DIST)/sdk -name '*.pyc' | xargs rm -f
+ (cd $(DIST)/sdk && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/sdk && tar -xf -)
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/include
+ (cd $(DIST)/include && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/include && tar -xf -)
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/idl
+ (cd $(DIST)/idl && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/idl && tar -xf -)
+ $(NSINSTALL) -D $(DIST)/$(MOZ_APP_NAME)-sdk/lib
+# sdk/lib is the same as sdk/sdk/lib
+ (cd $(DIST)/sdk/lib && $(TAR) $(TAR_CREATE_FLAGS) - .) | \
+ (cd $(DIST)/$(MOZ_APP_NAME)-sdk/lib && tar -xf -)
+ $(NSINSTALL) -D $(DIST)/$(SDK_PATH)
+ifndef PKG_SKIP_STRIP
+ USE_ELF_HACK= $(PYTHON) $(MOZILLA_DIR)/toolkit/mozapps/installer/strip.py $(DIST)/$(MOZ_APP_NAME)-sdk
+endif
+ cd $(DIST) && $(MAKE_SDK)
+ifdef UNIFY_DIST
+ifndef SDK_UNIFY
+ $(MAKE) -C $(UNIFY_DIST)/.. sdk SDK_UNIFY=1
+endif
+endif
+
+checksum:
+ mkdir -p `dirname $(CHECKSUM_FILE)`
+ @$(PYTHON) $(MOZILLA_DIR)/build/checksums.py \
+ -o $(CHECKSUM_FILE) \
+ $(CHECKSUM_ALGORITHM_PARAM) \
+ -s $(call QUOTED_WILDCARD,$(DIST)) \
+ $(UPLOAD_FILES)
+ @echo 'CHECKSUM FILE START'
+ @cat $(CHECKSUM_FILE)
+ @echo 'CHECKSUM FILE END'
+ $(SIGN_CHECKSUM_CMD)
+
+
+upload: checksum
+ $(PYTHON) -u $(MOZILLA_DIR)/build/upload.py --base-path $(DIST) \
+ --package '$(PACKAGE)' \
+ --properties-file $(DIST)/mach_build_properties.json \
+ $(UPLOAD_FILES) \
+ $(CHECKSUM_FILES)
+
+# source-package creates a source tarball from the files in MOZ_PKG_SRCDIR,
+# which is either set to a clean checkout or defaults to $topsrcdir
+source-package:
+ @echo 'Generate the sourcestamp file'
+ # Make sure to have repository information available and then generate the
+ # sourcestamp file.
+ $(MAKE) -C $(DEPTH) 'source-repo.h'
+ $(MAKE) make-sourcestamp-file
+ @echo 'Packaging source tarball...'
+ # We want to include the sourcestamp file in the source tarball, so copy it
+ # in the root source directory. This is useful to enable telemetry submissions
+ # from builds made from the source package with the correct revision information.
+ # Don't bother removing it as this is only used by automation.
+ @cp $(MOZ_SOURCESTAMP_FILE) '$(MOZ_PKG_SRCDIR)/sourcestamp.txt'
+ $(MKDIR) -p $(DIST)/$(PKG_SRCPACK_PATH)
+ (cd $(MOZ_PKG_SRCDIR) && $(CREATE_SOURCE_TAR) - ./ ) | xz -9e > $(SOURCE_TAR)
+
+hg-bundle:
+ $(MKDIR) -p $(DIST)/$(PKG_SRCPACK_PATH)
+ $(CREATE_HG_BUNDLE_CMD)
+
+source-upload:
+ $(MAKE) upload UPLOAD_FILES='$(SOURCE_UPLOAD_FILES)' CHECKSUM_FILE='$(SOURCE_CHECKSUM_FILE)'
diff --git a/toolkit/mozapps/installer/packager.py b/toolkit/mozapps/installer/packager.py
new file mode 100644
index 000000000..f2dc3fac6
--- /dev/null
+++ b/toolkit/mozapps/installer/packager.py
@@ -0,0 +1,415 @@
+# 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/.
+
+from mozpack.packager.formats import (
+ FlatFormatter,
+ JarFormatter,
+ OmniJarFormatter,
+)
+from mozpack.packager import (
+ preprocess_manifest,
+ preprocess,
+ Component,
+ SimpleManifestSink,
+)
+from mozpack.files import (
+ GeneratedFile,
+ FileFinder,
+ File,
+)
+from mozpack.copier import (
+ FileCopier,
+ Jarrer,
+)
+from mozpack.errors import errors
+from mozpack.unify import UnifiedBuildFinder
+import mozpack.path as mozpath
+import buildconfig
+from argparse import ArgumentParser
+import os
+from StringIO import StringIO
+import subprocess
+import platform
+import mozinfo
+
+# List of libraries to shlibsign.
+SIGN_LIBS = [
+ 'softokn3',
+ 'nssdbm3',
+ 'freebl3',
+ 'freeblpriv3',
+ 'freebl_32fpu_3',
+ 'freebl_32int_3',
+ 'freebl_32int64_3',
+ 'freebl_64fpu_3',
+ 'freebl_64int_3',
+]
+
+
+class ToolLauncher(object):
+ '''
+ Helper to execute tools like xpcshell with the appropriate environment.
+ launcher = ToolLauncher()
+ launcher.tooldir = '/path/to/tools'
+ launcher.launch(['xpcshell', '-e', 'foo.js'])
+ '''
+ def __init__(self):
+ self.tooldir = None
+
+ def launch(self, cmd, extra_linker_path=None, extra_env={}):
+ '''
+ Launch the given command, passed as a list. The first item in the
+ command list is the program name, without a path and without a suffix.
+ These are determined from the tooldir member and the BIN_SUFFIX value.
+ An extra_linker_path may be passed to give an additional directory
+ to add to the search paths for the dynamic linker.
+ An extra_env dict may be passed to give additional environment
+ variables to export when running the command.
+ '''
+ assert self.tooldir
+ cmd[0] = os.path.join(self.tooldir, 'bin',
+ cmd[0] + buildconfig.substs['BIN_SUFFIX'])
+ if not extra_linker_path:
+ extra_linker_path = os.path.join(self.tooldir, 'bin')
+ env = dict(os.environ)
+ for p in ['LD_LIBRARY_PATH', 'DYLD_LIBRARY_PATH']:
+ if p in env:
+ env[p] = extra_linker_path + ':' + env[p]
+ else:
+ env[p] = extra_linker_path
+ for e in extra_env:
+ env[e] = extra_env[e]
+
+ # For VC12+, make sure we can find the right bitness of pgort1x0.dll
+ if not buildconfig.substs.get('HAVE_64BIT_BUILD'):
+ for e in ('VS140COMNTOOLS', 'VS120COMNTOOLS'):
+ if e not in env:
+ continue
+
+ vcdir = os.path.abspath(os.path.join(env[e], '../../VC/bin'))
+ if os.path.exists(vcdir):
+ env['PATH'] = '%s;%s' % (vcdir, env['PATH'])
+ break
+
+ # Work around a bug in Python 2.7.2 and lower where unicode types in
+ # environment variables aren't handled by subprocess.
+ for k, v in env.items():
+ if isinstance(v, unicode):
+ env[k] = v.encode('utf-8')
+
+ print >>errors.out, 'Executing', ' '.join(cmd)
+ errors.out.flush()
+ return subprocess.call(cmd, env=env)
+
+ def can_launch(self):
+ return self.tooldir is not None
+
+launcher = ToolLauncher()
+
+
+class LibSignFile(File):
+ '''
+ File class for shlibsign signatures.
+ '''
+ def copy(self, dest, skip_if_older=True):
+ assert isinstance(dest, basestring)
+ # os.path.getmtime returns a result in seconds with precision up to the
+ # microsecond. But microsecond is too precise because shutil.copystat
+ # only copies milliseconds, and seconds is not enough precision.
+ if os.path.exists(dest) and skip_if_older and \
+ int(os.path.getmtime(self.path) * 1000) <= \
+ int(os.path.getmtime(dest) * 1000):
+ return False
+ if launcher.launch(['shlibsign', '-v', '-o', dest, '-i', self.path]):
+ errors.fatal('Error while signing %s' % self.path)
+
+
+def precompile_cache(registry, source_path, gre_path, app_path):
+ '''
+ Create startup cache for the given application directory, using the
+ given GRE path.
+ - registry is a FileRegistry-like instance where to add the startup cache.
+ - source_path is the base path of the package.
+ - gre_path is the GRE path, relative to source_path.
+ - app_path is the application path, relative to source_path.
+ Startup cache for all resources under resource://app/ are generated,
+ except when gre_path == app_path, in which case it's under
+ resource://gre/.
+ '''
+ from tempfile import mkstemp
+ source_path = os.path.abspath(source_path)
+ if app_path != gre_path:
+ resource = 'app'
+ else:
+ resource = 'gre'
+ app_path = os.path.join(source_path, app_path)
+ gre_path = os.path.join(source_path, gre_path)
+
+ fd, cache = mkstemp('.zip')
+ os.close(fd)
+ os.remove(cache)
+
+ try:
+ extra_env = {'MOZ_STARTUP_CACHE': cache}
+ if buildconfig.substs.get('MOZ_TSAN'):
+ extra_env['TSAN_OPTIONS'] = 'report_bugs=0'
+ if buildconfig.substs.get('MOZ_ASAN'):
+ extra_env['ASAN_OPTIONS'] = 'detect_leaks=0'
+ if launcher.launch(['xpcshell', '-g', gre_path, '-a', app_path,
+ '-f', os.path.join(os.path.dirname(__file__),
+ 'precompile_cache.js'),
+ '-e', 'precompile_startupcache("resource://%s/");'
+ % resource],
+ extra_linker_path=gre_path,
+ extra_env=extra_env):
+ errors.fatal('Error while running startup cache precompilation')
+ return
+ from mozpack.mozjar import JarReader
+ jar = JarReader(cache)
+ resource = '/resource/%s/' % resource
+ for f in jar:
+ if resource in f.filename:
+ path = f.filename[f.filename.index(resource) + len(resource):]
+ if registry.contains(path):
+ registry.add(f.filename, GeneratedFile(f.read()))
+ jar.close()
+ finally:
+ if os.path.exists(cache):
+ os.remove(cache)
+
+
+class RemovedFiles(GeneratedFile):
+ '''
+ File class for removed-files. Is used as a preprocessor parser.
+ '''
+ def __init__(self, copier):
+ self.copier = copier
+ GeneratedFile.__init__(self, '')
+
+ def handle_line(self, str):
+ f = str.strip()
+ if not f:
+ return
+ if self.copier.contains(f):
+ errors.error('Removal of packaged file(s): %s' % f)
+ self.content += f + '\n'
+
+
+def split_define(define):
+ '''
+ Give a VAR[=VAL] string, returns a (VAR, VAL) tuple, where VAL defaults to
+ 1. Numeric VALs are returned as ints.
+ '''
+ if '=' in define:
+ name, value = define.split('=', 1)
+ try:
+ value = int(value)
+ except ValueError:
+ pass
+ return (name, value)
+ return (define, 1)
+
+
+class NoPkgFilesRemover(object):
+ '''
+ Formatter wrapper to handle NO_PKG_FILES.
+ '''
+ def __init__(self, formatter, has_manifest):
+ assert 'NO_PKG_FILES' in os.environ
+ self._formatter = formatter
+ self._files = os.environ['NO_PKG_FILES'].split()
+ if has_manifest:
+ self._error = errors.error
+ self._msg = 'NO_PKG_FILES contains file listed in manifest: %s'
+ else:
+ self._error = errors.warn
+ self._msg = 'Skipping %s'
+
+ def add_base(self, base, *args):
+ self._formatter.add_base(base, *args)
+
+ def add(self, path, content):
+ if not any(mozpath.match(path, spec) for spec in self._files):
+ self._formatter.add(path, content)
+ else:
+ self._error(self._msg % path)
+
+ def add_manifest(self, entry):
+ self._formatter.add_manifest(entry)
+
+ def add_interfaces(self, path, content):
+ self._formatter.add_interfaces(path, content)
+
+ def contains(self, path):
+ return self._formatter.contains(path)
+
+
+def main():
+ parser = ArgumentParser()
+ parser.add_argument('-D', dest='defines', action='append',
+ metavar="VAR[=VAL]", help='Define a variable')
+ parser.add_argument('--format', default='omni',
+ help='Choose the chrome format for packaging ' +
+ '(omni, jar or flat ; default: %(default)s)')
+ parser.add_argument('--removals', default=None,
+ help='removed-files source file')
+ parser.add_argument('--ignore-errors', action='store_true', default=False,
+ help='Transform errors into warnings.')
+ parser.add_argument('--minify', action='store_true', default=False,
+ help='Make some files more compact while packaging')
+ parser.add_argument('--minify-js', action='store_true',
+ help='Minify JavaScript files while packaging.')
+ parser.add_argument('--js-binary',
+ help='Path to js binary. This is used to verify '
+ 'minified JavaScript. If this is not defined, '
+ 'minification verification will not be performed.')
+ parser.add_argument('--jarlog', default='', help='File containing jar ' +
+ 'access logs')
+ parser.add_argument('--optimizejars', action='store_true', default=False,
+ help='Enable jar optimizations')
+ parser.add_argument('--unify', default='',
+ help='Base directory of another build to unify with')
+ parser.add_argument('--disable-compression', action='store_false',
+ dest='compress', default=True,
+ help='Disable jar compression')
+ parser.add_argument('manifest', default=None, nargs='?',
+ help='Manifest file name')
+ parser.add_argument('source', help='Source directory')
+ parser.add_argument('destination', help='Destination directory')
+ parser.add_argument('--non-resource', nargs='+', metavar='PATTERN',
+ default=[],
+ help='Extra files not to be considered as resources')
+ args = parser.parse_args()
+
+ defines = dict(buildconfig.defines)
+ if args.ignore_errors:
+ errors.ignore_errors()
+
+ if args.defines:
+ for name, value in [split_define(d) for d in args.defines]:
+ defines[name] = value
+
+ copier = FileCopier()
+ if args.format == 'flat':
+ formatter = FlatFormatter(copier)
+ elif args.format == 'jar':
+ formatter = JarFormatter(copier, compress=args.compress, optimize=args.optimizejars)
+ elif args.format == 'omni':
+ formatter = OmniJarFormatter(copier,
+ buildconfig.substs['OMNIJAR_NAME'],
+ compress=args.compress,
+ optimize=args.optimizejars,
+ non_resources=args.non_resource)
+ else:
+ errors.fatal('Unknown format: %s' % args.format)
+
+ # Adjust defines according to the requested format.
+ if isinstance(formatter, OmniJarFormatter):
+ defines['MOZ_OMNIJAR'] = 1
+ elif 'MOZ_OMNIJAR' in defines:
+ del defines['MOZ_OMNIJAR']
+
+ respath = ''
+ if 'RESPATH' in defines:
+ respath = SimpleManifestSink.normalize_path(defines['RESPATH'])
+ while respath.startswith('/'):
+ respath = respath[1:]
+
+ if args.unify:
+ def is_native(path):
+ path = os.path.abspath(path)
+ return platform.machine() in mozpath.split(path)
+
+ # Invert args.unify and args.source if args.unify points to the
+ # native architecture.
+ args.source, args.unify = sorted([args.source, args.unify],
+ key=is_native, reverse=True)
+ if is_native(args.source) and not buildconfig.substs['CROSS_COMPILE']:
+ launcher.tooldir = args.source
+ elif not buildconfig.substs['CROSS_COMPILE']:
+ launcher.tooldir = mozpath.join(buildconfig.topobjdir, 'dist')
+
+ with errors.accumulate():
+ finder_args = dict(
+ minify=args.minify,
+ minify_js=args.minify_js,
+ )
+ if args.js_binary:
+ finder_args['minify_js_verify_command'] = [
+ args.js_binary,
+ os.path.join(os.path.abspath(os.path.dirname(__file__)),
+ 'js-compare-ast.js')
+ ]
+ if args.unify:
+ finder = UnifiedBuildFinder(FileFinder(args.source),
+ FileFinder(args.unify),
+ **finder_args)
+ else:
+ finder = FileFinder(args.source, **finder_args)
+ if 'NO_PKG_FILES' in os.environ:
+ sinkformatter = NoPkgFilesRemover(formatter,
+ args.manifest is not None)
+ else:
+ sinkformatter = formatter
+ sink = SimpleManifestSink(finder, sinkformatter)
+ if args.manifest:
+ preprocess_manifest(sink, args.manifest, defines)
+ else:
+ sink.add(Component(''), 'bin/*')
+ sink.close(args.manifest is not None)
+
+ if args.removals:
+ removals_in = StringIO(open(args.removals).read())
+ removals_in.name = args.removals
+ removals = RemovedFiles(copier)
+ preprocess(removals_in, removals, defines)
+ copier.add(mozpath.join(respath, 'removed-files'), removals)
+
+ # shlibsign libraries
+ if launcher.can_launch():
+ if not mozinfo.isMac and buildconfig.substs.get('COMPILE_ENVIRONMENT'):
+ for lib in SIGN_LIBS:
+ libbase = mozpath.join(respath, '%s%s') \
+ % (buildconfig.substs['DLL_PREFIX'], lib)
+ libname = '%s%s' % (libbase, buildconfig.substs['DLL_SUFFIX'])
+ if copier.contains(libname):
+ copier.add(libbase + '.chk',
+ LibSignFile(os.path.join(args.destination,
+ libname)))
+
+ # Setup preloading
+ if args.jarlog and os.path.exists(args.jarlog):
+ from mozpack.mozjar import JarLog
+ log = JarLog(args.jarlog)
+ for p, f in copier:
+ if not isinstance(f, Jarrer):
+ continue
+ key = JarLog.canonicalize(os.path.join(args.destination, p))
+ if key in log:
+ f.preload(log[key])
+
+ # Fill startup cache
+ if isinstance(formatter, OmniJarFormatter) and launcher.can_launch() \
+ and buildconfig.substs['MOZ_DISABLE_STARTUPCACHE'] != '1':
+ gre_path = None
+ def get_bases():
+ for b in sink.packager.get_bases(addons=False):
+ for p in (mozpath.join('bin', b), b):
+ if os.path.exists(os.path.join(args.source, p)):
+ yield p
+ break
+ for base in sorted(get_bases()):
+ if not gre_path:
+ gre_path = base
+ omnijar_path = mozpath.join(sink.normalize_path(base),
+ buildconfig.substs['OMNIJAR_NAME'])
+ if formatter.contains(omnijar_path):
+ precompile_cache(formatter.copier[omnijar_path],
+ args.source, gre_path, base)
+
+ copier.copy(args.destination)
+
+
+if __name__ == '__main__':
+ main()
diff --git a/toolkit/mozapps/installer/precompile_cache.js b/toolkit/mozapps/installer/precompile_cache.js
new file mode 100644
index 000000000..3eac93b65
--- /dev/null
+++ b/toolkit/mozapps/installer/precompile_cache.js
@@ -0,0 +1,87 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+// see http://mxr.mozilla.org/mozilla-central/source/services/sync/Weave.js#76
+
+var Cc = Components.classes;
+var Ci = Components.interfaces;
+var Cu = Components.utils;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const rph = Services.io.getProtocolHandler("resource").QueryInterface(Ci.nsIResProtocolHandler);
+
+function endsWith(str, end) {
+ return str.slice(-end.length) == end;
+}
+
+function jar_entries(jarReader, pattern) {
+ var entries = [];
+ var enumerator = jarReader.findEntries(pattern);
+ while (enumerator.hasMore()) {
+ entries.push(enumerator.getNext());
+ }
+ return entries;
+}
+
+function dir_entries(baseDir, subpath, ext) {
+ try {
+ var dir = baseDir.clone();
+ dir.append(subpath);
+ var enumerator = dir.directoryEntries;
+ } catch (e) {
+ return [];
+ }
+ var entries = [];
+ while (enumerator.hasMoreElements()) {
+ var file = enumerator.getNext().QueryInterface(Ci.nsIFile);
+ if (file.isDirectory()) {
+ entries = entries.concat(dir_entries(dir, file.leafName, ext).map(p => subpath + "/" + p));
+ } else if (endsWith(file.leafName, ext)) {
+ entries.push(subpath + "/" + file.leafName);
+ }
+ }
+ return entries;
+}
+
+function get_modules_under(uri) {
+ if (uri instanceof Ci.nsIJARURI) {
+ let jar = uri.QueryInterface(Ci.nsIJARURI);
+ let jarReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
+ let file = jar.JARFile.QueryInterface(Ci.nsIFileURL);
+ jarReader.open(file.file);
+ let entries = jar_entries(jarReader, "components/*.js")
+ .concat(jar_entries(jarReader, "modules/*.js"))
+ .concat(jar_entries(jarReader, "modules/*.jsm"));
+ jarReader.close();
+ return entries;
+ } else if (uri instanceof Ci.nsIFileURL) {
+ let file = uri.QueryInterface(Ci.nsIFileURL);
+ return dir_entries(file.file, "components", ".js")
+ .concat(dir_entries(file.file, "modules", ".js"))
+ .concat(dir_entries(file.file, "modules", ".jsm"));
+ }
+ throw new Error("Expected a nsIJARURI or nsIFileURL");
+}
+
+function load_modules_under(spec, uri) {
+ var entries = get_modules_under(uri).sort();
+ for (let entry of entries) {
+ try {
+ dump(spec + entry + "\n");
+ Cu.import(spec + entry, null);
+ } catch (e) {}
+ }
+}
+
+function resolveResource(spec) {
+ var uri = Services.io.newURI(spec, null, null);
+ return Services.io.newURI(rph.resolveURI(uri), null, null);
+}
+
+function precompile_startupcache(uri) {
+ load_modules_under(uri, resolveResource(uri));
+}
diff --git a/toolkit/mozapps/installer/signing.mk b/toolkit/mozapps/installer/signing.mk
new file mode 100644
index 000000000..0319e6983
--- /dev/null
+++ b/toolkit/mozapps/installer/signing.mk
@@ -0,0 +1,41 @@
+# 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/.
+
+# We shouldn't sign the first pass of a PGO build
+ifndef MOZ_PROFILE_GENERATE
+
+# Signing support
+ifdef MOZ_SIGN_CMD
+ifeq (WINNT,$(OS_ARCH))
+MOZ_INTERNAL_SIGNING_FORMAT := sha2signcode
+MOZ_EXTERNAL_SIGNING_FORMAT := sha2signcode
+MOZ_EXTERNAL_SIGNING_FORMAT_STUB := sha2signcodestub
+SIGN_INCLUDES := \
+ '*.dll' \
+ '*.exe' \
+ $(NULL)
+
+SIGN_EXCLUDES := \
+ 'D3DCompiler*.dll' \
+ 'msvc*.dll' \
+ $(NULL)
+endif # Windows
+
+ifeq (Darwin, $(OS_ARCH))
+MOZ_INTERNAL_SIGNING_FORMAT := macapp
+MOZ_EXTERNAL_SIGNING_FORMAT :=
+endif # Darwin
+
+ifeq (linux-gnu,$(TARGET_OS))
+MOZ_EXTERNAL_SIGNING_FORMAT :=
+endif # Linux
+
+ifdef MOZ_ASAN
+MOZ_INTERNAL_SIGNING_FORMAT :=
+MOZ_EXTERNAL_SIGNING_FORMAT :=
+endif
+
+endif # MOZ_SIGN_CMD
+
+endif # MOZ_PROFILE_GENERATE
diff --git a/toolkit/mozapps/installer/strip.py b/toolkit/mozapps/installer/strip.py
new file mode 100644
index 000000000..bb83025da
--- /dev/null
+++ b/toolkit/mozapps/installer/strip.py
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+'''
+Strip all files that can be stripped in the given directory.
+'''
+
+import sys
+from mozpack.files import FileFinder
+from mozpack.copier import FileCopier
+
+def strip(dir):
+ copier = FileCopier()
+ # The FileFinder will give use ExecutableFile instances for files
+ # that can be stripped, and copying ExecutableFiles defaults to
+ # stripping them unless buildconfig.substs['PKG_SKIP_STRIP'] is set.
+ for p, f in FileFinder(dir):
+ copier.add(p, f)
+ copier.copy(dir)
+
+if __name__ == '__main__':
+ strip(sys.argv[1])
diff --git a/toolkit/mozapps/installer/unpack.py b/toolkit/mozapps/installer/unpack.py
new file mode 100644
index 000000000..01a014422
--- /dev/null
+++ b/toolkit/mozapps/installer/unpack.py
@@ -0,0 +1,22 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+import sys
+import os
+from mozpack.packager.unpack import unpack
+import buildconfig
+
+
+def main():
+ if len(sys.argv) != 2:
+ print >>sys.stderr, "Usage: %s directory" % \
+ os.path.basename(sys.argv[0])
+ sys.exit(1)
+
+ buildconfig.substs['USE_ELF_HACK'] = False
+ buildconfig.substs['PKG_SKIP_STRIP'] = True
+ unpack(sys.argv[1])
+
+if __name__ == "__main__":
+ main()
diff --git a/toolkit/mozapps/installer/upload-files-APK.mk b/toolkit/mozapps/installer/upload-files-APK.mk
new file mode 100644
index 000000000..9bb5b2e18
--- /dev/null
+++ b/toolkit/mozapps/installer/upload-files-APK.mk
@@ -0,0 +1,141 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+# This file should ONLY be included from upload-files.mk. It was
+# split into its own file to increase comprehension of
+# upload-files.mk.
+
+include $(MOZILLA_DIR)/config/android-common.mk
+
+# Files packed into the APK root. Packing files into the APK root is not
+# supported by modern Android build systems, including Gradle, so don't add to
+# this list without Android peer approval.
+ROOT_FILES := \
+ application.ini \
+ package-name.txt \
+ ua-update.json \
+ platform.ini \
+ removed-files \
+ $(NULL)
+
+GECKO_APP_AP_PATH = $(topobjdir)/mobile/android/base
+
+ifdef ENABLE_TESTS
+INNER_ROBOCOP_PACKAGE=true
+ifeq ($(MOZ_BUILD_APP),mobile/android)
+UPLOAD_EXTRA_FILES += robocop.apk
+
+# Robocop/Robotium tests, Android Background tests, and Fennec need to
+# be signed with the same key, which means release signing them all.
+
+ifndef MOZ_BUILD_MOBILE_ANDROID_WITH_GRADLE
+robocop_apk := $(topobjdir)/mobile/android/tests/browser/robocop/robocop-debug-unsigned-unaligned.apk
+else
+robocop_apk := $(topobjdir)/gradle/build/mobile/android/app/outputs/apk/app-automation-debug-androidTest-unaligned.apk
+endif
+
+INNER_ROBOCOP_PACKAGE= \
+ $(call RELEASE_SIGN_ANDROID_APK,$(robocop_apk),$(ABS_DIST)/robocop.apk)
+endif
+else
+INNER_ROBOCOP_PACKAGE=echo 'Testing is disabled - No Android Robocop for you'
+endif
+
+ifdef MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER
+INNER_INSTALL_BOUNCER_PACKAGE=true
+ifdef ENABLE_TESTS
+UPLOAD_EXTRA_FILES += bouncer.apk
+
+bouncer_package=$(ABS_DIST)/bouncer.apk
+
+# Package and release sign the install bouncer APK. This assumes that the main
+# APK (that is, $(PACKAGE)) has already been produced, and verifies that the
+# bouncer APK and the main APK define the same set of permissions. The
+# intention is to avoid permission-related surprises when bouncing to the
+# installation process in the Play Store. N.b.: sort -u is Posix and saves
+# invoking uniq separately. diff -u is *not* Posix, so we only add -c.
+INNER_INSTALL_BOUNCER_PACKAGE=\
+ $(call RELEASE_SIGN_ANDROID_APK,$(topobjdir)/mobile/android/bouncer/bouncer-unsigned-unaligned.apk,$(bouncer_package)) && \
+ ($(AAPT) dump permissions $(PACKAGE) | sort -u > $(PACKAGE).permissions && \
+ $(AAPT) dump permissions $(bouncer_package) | sort -u > $(bouncer_package).permissions && \
+ diff -c $(PACKAGE).permissions $(bouncer_package).permissions || \
+ (echo "*** Error: The permissions of the bouncer package differ from the permissions of the main package. Ensure the bouncer and main package Android manifests agree, rebuild mobile/android, and re-package." && exit 1))
+else
+INNER_INSTALL_BOUNCER_PACKAGE=echo 'Testing is disabled, so the install bouncer is disabled - No trampolines for you'
+endif # ENABLE_TESTS
+else
+INNER_INSTALL_BOUNCER_PACKAGE=echo 'Install bouncer is disabled - No trampolines for you'
+endif # MOZ_ANDROID_PACKAGE_INSTALL_BOUNCER
+
+# Fennec's OMNIJAR_NAME can include a directory; for example, it might
+# be "assets/omni.ja". This path specifies where the omni.ja file
+# lives in the APK, but should not root the resources it contains
+# under assets/ (i.e., resources should not live at chrome://assets/).
+# packager.py writes /omni.ja in order to be consistent with the
+# layout expected by language repacks. Therefore, we move it to the
+# correct path here, in INNER_MAKE_PACKAGE. See comment about
+# OMNIJAR_NAME in configure.in.
+
+# OMNIJAR_DIR is './' for "omni.ja", 'assets/' for "assets/omni.ja".
+OMNIJAR_DIR := $(dir $(OMNIJAR_NAME))
+OMNIJAR_NAME := $(notdir $(OMNIJAR_NAME))
+
+# We force build an ap_ that does not check dependencies below.
+# Language repacks take advantage of this unchecked dependency ap_ to
+# insert additional resources (translated strings) into the ap_
+# without the build system's participation. This can do the wrong
+# thing if there are resource changes in between build time and
+# package time.
+PKG_SUFFIX = .apk
+
+INNER_FENNEC_PACKAGE = \
+ $(MAKE) -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
+ $(PYTHON) -m mozbuild.action.package_fennec_apk \
+ --verbose \
+ --inputs \
+ $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ \
+ --omnijar $(STAGEPATH)$(MOZ_PKG_DIR)/$(OMNIJAR_NAME) \
+ --classes-dex $(GECKO_APP_AP_PATH)/classes.dex \
+ --lib-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/lib \
+ --assets-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/assets \
+ --features-dirs $(STAGEPATH)$(MOZ_PKG_DIR)/features \
+ --root-files $(foreach f,$(ROOT_FILES),$(STAGEPATH)$(MOZ_PKG_DIR)/$(f)) \
+ --output $(PACKAGE:.apk=-unsigned-unaligned.apk) && \
+ $(call RELEASE_SIGN_ANDROID_APK,$(PACKAGE:.apk=-unsigned-unaligned.apk),$(PACKAGE))
+
+# Packaging produces many optional artifacts.
+package_fennec = \
+ $(INNER_FENNEC_PACKAGE) && \
+ $(INNER_ROBOCOP_PACKAGE) && \
+ $(INNER_INSTALL_BOUNCER_PACKAGE)
+
+# Re-packaging only replaces Android resources and the omnijar before
+# (re-)signing.
+repackage_fennec = \
+ $(MAKE) -C $(GECKO_APP_AP_PATH) gecko-nodeps.ap_ && \
+ $(PYTHON) -m mozbuild.action.package_fennec_apk \
+ --verbose \
+ --inputs \
+ $(UNPACKAGE) \
+ $(GECKO_APP_AP_PATH)/gecko-nodeps.ap_ \
+ --omnijar $(STAGEPATH)$(MOZ_PKG_DIR)/$(OMNIJAR_NAME) \
+ --output $(PACKAGE:.apk=-unsigned-unaligned.apk) && \
+ $(call RELEASE_SIGN_ANDROID_APK,$(PACKAGE:.apk=-unsigned-unaligned.apk),$(PACKAGE))
+
+INNER_MAKE_PACKAGE = $(if $(UNPACKAGE),$(repackage_fennec),$(package_fennec))
+
+# Language repacks root the resources contained in assets/omni.ja
+# under assets/, but the repacks expect them to be rooted at /.
+# Therefore, we we move the omnijar back to / so the resources are
+# under the root here, in INNER_UNMAKE_PACKAGE. See comments about
+# OMNIJAR_NAME earlier in this file and in configure.in.
+
+INNER_UNMAKE_PACKAGE = \
+ mkdir $(MOZ_PKG_DIR) && \
+ ( cd $(MOZ_PKG_DIR) && \
+ $(UNZIP) $(UNPACKAGE) $(ROOT_FILES) && \
+ $(UNZIP) $(UNPACKAGE) $(OMNIJAR_DIR)$(OMNIJAR_NAME) && \
+ $(if $(filter-out ./,$(OMNIJAR_DIR)), \
+ mv $(OMNIJAR_DIR)$(OMNIJAR_NAME) $(OMNIJAR_NAME), \
+ true) )
diff --git a/toolkit/mozapps/installer/upload-files.mk b/toolkit/mozapps/installer/upload-files.mk
new file mode 100644
index 000000000..516331782
--- /dev/null
+++ b/toolkit/mozapps/installer/upload-files.mk
@@ -0,0 +1,529 @@
+# 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_PKG_FORMAT
+ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+MOZ_PKG_FORMAT = DMG
+else
+ifeq (,$(filter-out WINNT, $(OS_ARCH)))
+MOZ_PKG_FORMAT = ZIP
+else
+ifeq (,$(filter-out SunOS, $(OS_ARCH)))
+ MOZ_PKG_FORMAT = BZ2
+else
+ ifeq (,$(filter-out gtk2 gtk3 qt, $(MOZ_WIDGET_TOOLKIT)))
+ MOZ_PKG_FORMAT = BZ2
+ else
+ ifeq (android,$(MOZ_WIDGET_TOOLKIT))
+ MOZ_PKG_FORMAT = APK
+ else
+ MOZ_PKG_FORMAT = TGZ
+ endif
+ endif
+endif
+endif
+endif
+endif # MOZ_PKG_FORMAT
+
+ifeq ($(OS_ARCH),WINNT)
+INSTALLER_DIR = windows
+endif
+
+ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+ifndef _APPNAME
+_APPNAME = $(MOZ_MACBUNDLE_NAME)
+endif
+ifndef _BINPATH
+_BINPATH = /$(_APPNAME)/Contents/MacOS
+endif # _BINPATH
+ifndef _RESPATH
+# Resource path for the precomplete file
+_RESPATH = /$(_APPNAME)/Contents/Resources
+endif
+ifdef UNIVERSAL_BINARY
+STAGEPATH = universal/
+endif
+endif
+
+PACKAGE_BASE_DIR = $(ABS_DIST)
+PACKAGE = $(PKG_PATH)$(PKG_BASENAME)$(PKG_SUFFIX)
+
+# By default, the SDK uses the same packaging type as the main bundle,
+# but on mac it is a .tar.bz2
+SDK_SUFFIX = $(PKG_SUFFIX)
+SDK = $(SDK_PATH)$(PKG_BASENAME).sdk$(SDK_SUFFIX)
+ifdef UNIVERSAL_BINARY
+SDK = $(SDK_PATH)$(PKG_BASENAME)-$(TARGET_CPU).sdk$(SDK_SUFFIX)
+endif
+
+# JavaScript Shell packaging
+JSSHELL_BINS = \
+ js$(BIN_SUFFIX) \
+ $(DLL_PREFIX)mozglue$(DLL_SUFFIX) \
+ $(NULL)
+
+ifndef MOZ_SYSTEM_NSPR
+ ifdef MOZ_FOLD_LIBS
+ JSSHELL_BINS += $(DLL_PREFIX)nss3$(DLL_SUFFIX)
+ else
+ JSSHELL_BINS += \
+ $(DLL_PREFIX)nspr4$(DLL_SUFFIX) \
+ $(DLL_PREFIX)plds4$(DLL_SUFFIX) \
+ $(DLL_PREFIX)plc4$(DLL_SUFFIX) \
+ $(NULL)
+ endif # MOZ_FOLD_LIBS
+endif # MOZ_SYSTEM_NSPR
+
+ifdef MSVC_C_RUNTIME_DLL
+ JSSHELL_BINS += $(MSVC_C_RUNTIME_DLL)
+endif
+ifdef MSVC_CXX_RUNTIME_DLL
+ JSSHELL_BINS += $(MSVC_CXX_RUNTIME_DLL)
+endif
+
+ifdef WIN_UCRT_REDIST_DIR
+ JSSHELL_BINS += $(notdir $(wildcard $(DIST)/bin/api-ms-win-*.dll))
+ JSSHELL_BINS += ucrtbase.dll
+endif
+
+MAKE_JSSHELL = $(call py_action,zip,-C $(DIST)/bin $(abspath $(PKG_JSSHELL)) $(JSSHELL_BINS))
+
+JARLOG_DIR = $(topobjdir)/jarlog/
+JARLOG_FILE_AB_CD = $(JARLOG_DIR)/$(AB_CD).log
+
+TAR_CREATE_FLAGS := --exclude=.mkdir.done $(TAR_CREATE_FLAGS)
+CREATE_FINAL_TAR = $(TAR) -c --owner=0 --group=0 --numeric-owner \
+ --mode=go-w --exclude=.mkdir.done -f
+UNPACK_TAR = tar -xf-
+
+ifeq ($(MOZ_PKG_FORMAT),TAR)
+ PKG_SUFFIX = .tar
+ INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) > $(PACKAGE)
+ INNER_UNMAKE_PACKAGE = $(UNPACK_TAR) < $(UNPACKAGE)
+ MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk > '$(SDK)'
+endif
+
+ifeq ($(MOZ_PKG_FORMAT),TGZ)
+ PKG_SUFFIX = .tar.gz
+ INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) | gzip -vf9 > $(PACKAGE)
+ INNER_UNMAKE_PACKAGE = gunzip -c $(UNPACKAGE) | $(UNPACK_TAR)
+ MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | gzip -vf9 > '$(SDK)'
+endif
+
+ifeq ($(MOZ_PKG_FORMAT),BZ2)
+ PKG_SUFFIX = .tar.bz2
+ ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+ INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - -C $(STAGEPATH)$(MOZ_PKG_DIR) $(_APPNAME) | bzip2 -vf > $(PACKAGE)
+ else
+ INNER_MAKE_PACKAGE = $(CREATE_FINAL_TAR) - $(MOZ_PKG_DIR) | bzip2 -vf > $(PACKAGE)
+ endif
+ INNER_UNMAKE_PACKAGE = bunzip2 -c $(UNPACKAGE) | $(UNPACK_TAR)
+ MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > '$(SDK)'
+endif
+
+ifeq ($(MOZ_PKG_FORMAT),ZIP)
+ ifdef MOZ_EXTERNAL_SIGNING_FORMAT
+ # We can't use sha2signcode on zip files
+ MOZ_EXTERNAL_SIGNING_FORMAT := $(filter-out sha2signcode,$(MOZ_EXTERNAL_SIGNING_FORMAT))
+ endif
+ PKG_SUFFIX = .zip
+ INNER_MAKE_PACKAGE = $(ZIP) -r9D $(PACKAGE) $(MOZ_PKG_DIR) \
+ -x \*/.mkdir.done
+ INNER_UNMAKE_PACKAGE = $(UNZIP) $(UNPACKAGE)
+ MAKE_SDK = $(call py_action,zip,'$(SDK)' $(MOZ_APP_NAME)-sdk)
+endif
+
+ifeq ($(MOZ_PKG_FORMAT),SFX7Z)
+ PKG_SUFFIX = .exe
+ INNER_MAKE_PACKAGE = rm -f app.7z && \
+ mv $(MOZ_PKG_DIR) core && \
+ $(CYGWIN_WRAPPER) 7z a -r -t7z app.7z -mx -m0=BCJ2 -m1=LZMA:d25 \
+ -m2=LZMA:d19 -m3=LZMA:d19 -mb0:1 -mb0s1:2 -mb0s2:3 && \
+ mv core $(MOZ_PKG_DIR) && \
+ cat $(SFX_HEADER) app.7z > $(PACKAGE) && \
+ chmod 0755 $(PACKAGE)
+ INNER_UNMAKE_PACKAGE = $(CYGWIN_WRAPPER) 7z x $(UNPACKAGE) core && \
+ mv core $(MOZ_PKG_DIR)
+endif
+
+#Create an RPM file
+ifeq ($(MOZ_PKG_FORMAT),RPM)
+ PKG_SUFFIX = .rpm
+ MOZ_NUMERIC_APP_VERSION = $(shell echo $(MOZ_PKG_VERSION) | sed 's/[^0-9.].*//' )
+ MOZ_RPM_RELEASE = $(shell echo $(MOZ_PKG_VERSION) | sed 's/[0-9.]*//' )
+
+ RPMBUILD_TOPDIR=$(ABS_DIST)/rpmbuild
+ RPMBUILD_RPMDIR=$(ABS_DIST)
+ RPMBUILD_SRPMDIR=$(ABS_DIST)
+ RPMBUILD_SOURCEDIR=$(RPMBUILD_TOPDIR)/SOURCES
+ RPMBUILD_SPECDIR=$(topsrcdir)/toolkit/mozapps/installer/linux/rpm
+ RPMBUILD_BUILDDIR=$(ABS_DIST)/..
+
+ SPEC_FILE = $(RPMBUILD_SPECDIR)/mozilla.spec
+ RPM_INCIDENTALS=$(topsrcdir)/toolkit/mozapps/installer/linux/rpm
+
+ RPM_CMD = \
+ echo Creating RPM && \
+ $(PYTHON) -m mozbuild.action.preprocessor \
+ -DMOZ_APP_NAME=$(MOZ_APP_NAME) \
+ -DMOZ_APP_DISPLAYNAME='$(MOZ_APP_DISPLAYNAME)' \
+ $(RPM_INCIDENTALS)/mozilla.desktop \
+ -o $(RPMBUILD_SOURCEDIR)/$(MOZ_APP_NAME).desktop && \
+ rm -rf $(ABS_DIST)/$(TARGET_CPU) && \
+ $(RPMBUILD) -bb \
+ $(SPEC_FILE) \
+ --target $(TARGET_CPU) \
+ --buildroot $(RPMBUILD_TOPDIR)/BUILDROOT \
+ --define 'moz_app_name $(MOZ_APP_NAME)' \
+ --define 'moz_app_displayname $(MOZ_APP_DISPLAYNAME)' \
+ --define 'moz_app_version $(MOZ_APP_VERSION)' \
+ --define 'moz_numeric_app_version $(MOZ_NUMERIC_APP_VERSION)' \
+ --define 'moz_rpm_release $(MOZ_RPM_RELEASE)' \
+ --define 'buildid $(BUILDID)' \
+ --define 'moz_source_repo $(shell awk '$$2 == "MOZ_SOURCE_REPO" {print $$3}' $(DEPTH)/source-repo.h)' \
+ --define 'moz_source_stamp $(shell awk '$$2 == "MOZ_SOURCE_STAMP" {print $$3}' $(DEPTH)/source-repo.h)' \
+ --define 'moz_branding_directory $(topsrcdir)/$(MOZ_BRANDING_DIRECTORY)' \
+ --define '_topdir $(RPMBUILD_TOPDIR)' \
+ --define '_rpmdir $(RPMBUILD_RPMDIR)' \
+ --define '_sourcedir $(RPMBUILD_SOURCEDIR)' \
+ --define '_specdir $(RPMBUILD_SPECDIR)' \
+ --define '_srcrpmdir $(RPMBUILD_SRPMDIR)' \
+ --define '_builddir $(RPMBUILD_BUILDDIR)' \
+ --define '_prefix $(prefix)' \
+ --define '_libdir $(libdir)' \
+ --define '_bindir $(bindir)' \
+ --define '_datadir $(datadir)' \
+ --define '_installdir $(installdir)'
+
+ ifdef ENABLE_TESTS
+ RPM_CMD += \
+ --define 'createtests yes' \
+ --define '_testsinstalldir $(shell basename $(installdir))'
+ endif
+
+ ifdef INSTALL_SDK
+ RPM_CMD += \
+ --define 'createdevel yes' \
+ --define '_idldir $(idldir)' \
+ --define '_sdkdir $(sdkdir)' \
+ --define '_includedir $(includedir)'
+ endif
+
+ #For each of the main, tests, sdk rpms we want to make sure that
+ #if they exist that they are in objdir/dist/ and that they get
+ #uploaded and that they are beside the other build artifacts
+ MAIN_RPM= $(MOZ_APP_NAME)-$(MOZ_NUMERIC_APP_VERSION)-$(MOZ_RPM_RELEASE).$(BUILDID).$(TARGET_CPU)$(PKG_SUFFIX)
+ UPLOAD_EXTRA_FILES += $(MAIN_RPM)
+ RPM_CMD += && mv $(TARGET_CPU)/$(MAIN_RPM) $(ABS_DIST)/
+
+ ifdef ENABLE_TESTS
+ TESTS_RPM=$(MOZ_APP_NAME)-tests-$(MOZ_NUMERIC_APP_VERSION)-$(MOZ_RPM_RELEASE).$(BUILDID).$(TARGET_CPU)$(PKG_SUFFIX)
+ UPLOAD_EXTRA_FILES += $(TESTS_RPM)
+ RPM_CMD += && mv $(TARGET_CPU)/$(TESTS_RPM) $(ABS_DIST)/
+ endif
+
+ ifdef INSTALL_SDK
+ SDK_RPM=$(MOZ_APP_NAME)-devel-$(MOZ_NUMERIC_APP_VERSION)-$(MOZ_RPM_RELEASE).$(BUILDID).$(TARGET_CPU)$(PKG_SUFFIX)
+ UPLOAD_EXTRA_FILES += $(SDK_RPM)
+ RPM_CMD += && mv $(TARGET_CPU)/$(SDK_RPM) $(ABS_DIST)/
+ endif
+
+ INNER_MAKE_PACKAGE = $(RPM_CMD)
+ #Avoiding rpm repacks, going to try creating/uploading xpi in rpm files instead
+ INNER_UNMAKE_PACKAGE = $(error Try using rpm2cpio and cpio)
+
+endif #Create an RPM file
+
+
+ifeq ($(MOZ_PKG_FORMAT),APK)
+include $(MOZILLA_DIR)/toolkit/mozapps/installer/upload-files-$(MOZ_PKG_FORMAT).mk
+endif
+
+ifeq ($(MOZ_PKG_FORMAT),DMG)
+ PKG_SUFFIX = .dmg
+
+ _ABS_MOZSRCDIR = $(shell cd $(MOZILLA_DIR) && pwd)
+ PKG_DMG_SOURCE = $(STAGEPATH)$(MOZ_PKG_DIR)
+ INNER_MAKE_PACKAGE = $(call py_action,make_dmg,'$(PKG_DMG_SOURCE)' '$(PACKAGE)')
+ INNER_UNMAKE_PACKAGE = \
+ set -ex; \
+ rm -rf $(ABS_DIST)/unpack.tmp; \
+ mkdir -p $(ABS_DIST)/unpack.tmp; \
+ $(_ABS_MOZSRCDIR)/build/package/mac_osx/unpack-diskimage $(UNPACKAGE) /tmp/$(MOZ_PKG_APPNAME)-unpack $(ABS_DIST)/unpack.tmp; \
+ rsync -a '$(ABS_DIST)/unpack.tmp/$(_APPNAME)' $(MOZ_PKG_DIR); \
+ test -n '$(MOZ_PKG_MAC_DSSTORE)' && \
+ rsync -a '$(ABS_DIST)/unpack.tmp/.DS_Store' '$(MOZ_PKG_MAC_DSSTORE)'; \
+ test -n '$(MOZ_PKG_MAC_BACKGROUND)' && \
+ rsync -a '$(ABS_DIST)/unpack.tmp/.background/$(notdir $(MOZ_PKG_MAC_BACKGROUND))' '$(MOZ_PKG_MAC_BACKGROUND)'; \
+ test -n '$(MOZ_PKG_MAC_ICON)' && \
+ rsync -a '$(ABS_DIST)/unpack.tmp/.VolumeIcon.icns' '$(MOZ_PKG_MAC_ICON)'; \
+ rm -rf $(ABS_DIST)/unpack.tmp; \
+ if test -n '$(MOZ_PKG_MAC_RSRC)' ; then \
+ cp $(UNPACKAGE) $(MOZ_PKG_APPNAME).tmp.dmg && \
+ hdiutil unflatten $(MOZ_PKG_APPNAME).tmp.dmg && \
+ { /Developer/Tools/DeRez -skip plst -skip blkx $(MOZ_PKG_APPNAME).tmp.dmg > '$(MOZ_PKG_MAC_RSRC)' || { rm -f $(MOZ_PKG_APPNAME).tmp.dmg && false; }; } && \
+ rm -f $(MOZ_PKG_APPNAME).tmp.dmg; \
+ fi
+ # The plst and blkx resources are skipped because they belong to each
+ # individual dmg and are created by hdiutil.
+ SDK_SUFFIX = .tar.bz2
+ MAKE_SDK = $(CREATE_FINAL_TAR) - $(MOZ_APP_NAME)-sdk | bzip2 -vf > '$(SDK)'
+endif
+
+ifdef MOZ_INTERNAL_SIGNING_FORMAT
+ MOZ_SIGN_PREPARED_PACKAGE_CMD=$(MOZ_SIGN_CMD) $(foreach f,$(MOZ_INTERNAL_SIGNING_FORMAT),-f $(f)) $(foreach i,$(SIGN_INCLUDES),-i $(i)) $(foreach x,$(SIGN_EXCLUDES),-x $(x))
+ ifeq (WINNT,$(OS_ARCH))
+ MOZ_SIGN_PREPARED_PACKAGE_CMD += --nsscmd '$(ABS_DIST)/bin/shlibsign$(BIN_SUFFIX) -v -i'
+ endif
+endif
+
+# For final GPG / authenticode signing / dmg signing if required
+ifdef MOZ_EXTERNAL_SIGNING_FORMAT
+ MOZ_SIGN_PACKAGE_CMD=$(MOZ_SIGN_CMD) $(foreach f,$(MOZ_EXTERNAL_SIGNING_FORMAT),-f $(f))
+endif
+
+ifdef MOZ_SIGN_PREPARED_PACKAGE_CMD
+ ifeq (Darwin, $(OS_ARCH))
+ MAKE_PACKAGE = $(or $(call MAKE_SIGN_EME_VOUCHER,$(STAGEPATH)$(MOZ_PKG_DIR)$(_BINPATH)/$(MOZ_CHILD_PROCESS_NAME).app/Contents/MacOS,$(STAGEPATH)$(MOZ_PKG_DIR)$(_RESPATH)),true) \
+ && (cd $(STAGEPATH)$(MOZ_PKG_DIR)$(_RESPATH) && $(CREATE_PRECOMPLETE_CMD)) \
+ && cd ./$(PKG_DMG_SOURCE) && $(MOZ_SIGN_PREPARED_PACKAGE_CMD) $(MOZ_MACBUNDLE_NAME) \
+ && cd $(PACKAGE_BASE_DIR) && $(INNER_MAKE_PACKAGE)
+ else
+ MAKE_PACKAGE = $(MOZ_SIGN_PREPARED_PACKAGE_CMD) $(MOZ_PKG_DIR) \
+ && $(or $(call MAKE_SIGN_EME_VOUCHER,$(STAGEPATH)$(MOZ_PKG_DIR)),true) \
+ && (cd $(STAGEPATH)$(MOZ_PKG_DIR)$(_RESPATH) && $(CREATE_PRECOMPLETE_CMD)) \
+ && $(INNER_MAKE_PACKAGE)
+ endif #Darwin
+
+else
+ MAKE_PACKAGE = (cd $(STAGEPATH)$(MOZ_PKG_DIR)$(_RESPATH) && $(CREATE_PRECOMPLETE_CMD)) && $(INNER_MAKE_PACKAGE)
+endif
+
+ifdef MOZ_SIGN_PACKAGE_CMD
+ MAKE_PACKAGE += && $(MOZ_SIGN_PACKAGE_CMD) '$(PACKAGE)'
+endif
+
+ifdef MOZ_SIGN_CMD
+ MAKE_SDK += && $(MOZ_SIGN_CMD) -f gpg '$(SDK)'
+endif
+
+NO_PKG_FILES += \
+ core \
+ bsdecho \
+ js \
+ js-config \
+ jscpucfg \
+ nsinstall \
+ viewer \
+ TestGtkEmbed \
+ elf-dynstr-gc \
+ mangle* \
+ maptsv* \
+ mfc* \
+ msdump* \
+ msmap* \
+ nm2tsv* \
+ nsinstall* \
+ res/samples \
+ res/throbber \
+ shlibsign* \
+ certutil* \
+ pk12util* \
+ BadCertServer* \
+ OCSPStaplingServer* \
+ GenerateOCSPResponse* \
+ chrome/chrome.rdf \
+ chrome/app-chrome.manifest \
+ chrome/overlayinfo \
+ components/compreg.dat \
+ components/xpti.dat \
+ content_unit_tests \
+ necko_unit_tests \
+ *.dSYM \
+ $(NULL)
+
+# If a manifest has not been supplied, the following
+# files should be excluded from the package too
+ifndef MOZ_PKG_MANIFEST
+ NO_PKG_FILES += ssltunnel*
+endif
+
+ifdef MOZ_DMD
+ NO_PKG_FILES += SmokeDMD
+endif
+
+DEFINES += -DDLL_PREFIX=$(DLL_PREFIX) -DDLL_SUFFIX=$(DLL_SUFFIX) -DBIN_SUFFIX=$(BIN_SUFFIX)
+
+ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+ DEFINES += -DDIR_MACOS=Contents/MacOS/ -DDIR_RESOURCES=Contents/Resources/
+else
+ DEFINES += -DDIR_MACOS= -DDIR_RESOURCES=
+endif
+
+ifdef MOZ_FOLD_LIBS
+ DEFINES += -DMOZ_FOLD_LIBS=1
+endif
+
+GARBAGE += $(DIST)/$(PACKAGE) $(PACKAGE)
+
+# The following target stages files into two directories: one directory for
+# core files, and one for optional extensions based on the information in
+# the MOZ_PKG_MANIFEST file.
+
+PKG_ARG = , '$(pkg)'
+
+# MOZ_PKG_MANIFEST is the canonical way to define the package manifest (which
+# the packager will preprocess), but for a smooth transition, we derive it
+# from the now deprecated MOZ_PKG_MANIFEST_P when MOZ_PKG_MANIFEST is not
+# defined.
+ifndef MOZ_PKG_MANIFEST
+ ifdef MOZ_PKG_MANIFEST_P
+ MOZ_PKG_MANIFEST := $(MOZ_PKG_MANIFEST_P)
+ endif # MOZ_PKG_MANIFEST_P
+endif # MOZ_PKG_MANIFEST
+
+ifndef MOZ_PACKAGER_FORMAT
+ MOZ_PACKAGER_FORMAT = $(error MOZ_PACKAGER_FORMAT is not set)
+endif
+
+ifneq (android,$(MOZ_WIDGET_TOOLKIT))
+ OPTIMIZEJARS = 1
+ ifneq (gonk,$(MOZ_WIDGET_TOOLKIT))
+ ifdef NIGHTLY_BUILD
+ DISABLE_JAR_COMPRESSION = 1
+ endif
+ endif
+endif
+
+# A js binary is needed to perform verification of JavaScript minification.
+# We can only use the built binary when not cross-compiling. Environments
+# (such as release automation) can provide their own js binary to enable
+# verification when cross-compiling.
+ifndef JS_BINARY
+ ifndef CROSS_COMPILE
+ JS_BINARY = $(wildcard $(DIST)/bin/js)
+ endif
+endif
+
+ifeq ($(OS_TARGET), WINNT)
+ INSTALLER_PACKAGE = $(DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe
+endif
+
+# These are necessary because some of our packages/installers contain spaces
+# in their filenames and GNU Make's $(wildcard) function doesn't properly
+# deal with them.
+empty :=
+space = $(empty) $(empty)
+QUOTED_WILDCARD = $(if $(wildcard $(subst $(space),?,$(1))),'$(1)')
+ESCAPE_SPACE = $(subst $(space),\$(space),$(1))
+ESCAPE_WILDCARD = $(subst $(space),?,$(1))
+
+# This variable defines which OpenSSL algorithm to use to
+# generate checksums for files that we upload
+CHECKSUM_ALGORITHM_PARAM = -d sha512 -d md5 -d sha1
+
+# This variable defines where the checksum file will be located
+CHECKSUM_FILE = '$(DIST)/$(PKG_PATH)/$(CHECKSUMS_FILE_BASENAME).checksums'
+CHECKSUM_FILES = $(CHECKSUM_FILE)
+
+# Upload MAR tools only if AB_CD is unset or en_US
+ifeq (,$(AB_CD:en-US=))
+ ifeq (WINNT,$(OS_TARGET))
+ UPLOAD_EXTRA_FILES += host/bin/mar.exe
+ UPLOAD_EXTRA_FILES += host/bin/mbsdiff.exe
+ else
+ UPLOAD_EXTRA_FILES += host/bin/mar
+ UPLOAD_EXTRA_FILES += host/bin/mbsdiff
+ endif
+endif
+
+UPLOAD_FILES= \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(INSTALLER_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(COMPLETE_MAR)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(LANGPACK)) \
+ $(call QUOTED_WILDCARD,$(wildcard $(DIST)/$(PARTIAL_MAR))) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(MOZHARNESS_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(TEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(CPP_TEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(XPC_TEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(MOCHITEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(TALOS_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(REFTEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(WP_TEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(GTEST_PACKAGE)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(SYMBOL_ARCHIVE_BASENAME).zip) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(SDK)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(SDK).asc) \
+ $(call QUOTED_WILDCARD,$(MOZ_SOURCESTAMP_FILE)) \
+ $(call QUOTED_WILDCARD,$(MOZ_BUILDINFO_FILE)) \
+ $(call QUOTED_WILDCARD,$(MOZ_BUILDID_INFO_TXT_FILE)) \
+ $(call QUOTED_WILDCARD,$(MOZ_MOZINFO_FILE)) \
+ $(call QUOTED_WILDCARD,$(MOZ_TEST_PACKAGES_FILE)) \
+ $(call QUOTED_WILDCARD,$(PKG_JSSHELL)) \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(SYMBOL_FULL_ARCHIVE_BASENAME).zip) \
+ $(if $(UPLOAD_EXTRA_FILES), $(foreach f, $(UPLOAD_EXTRA_FILES), $(wildcard $(DIST)/$(f))))
+
+ifdef MOZ_CODE_COVERAGE
+ UPLOAD_FILES += \
+ $(call QUOTED_WILDCARD,$(DIST)/$(PKG_PATH)$(CODE_COVERAGE_ARCHIVE_BASENAME).zip)
+endif
+
+ifdef UNIFY_DIST
+ UNIFY_ARCH := $(notdir $(patsubst %/,%,$(dir $(UNIFY_DIST))))
+ UPLOAD_FILES += \
+ $(call QUOTED_WILDCARD,$(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX)) \
+ $(call QUOTED_WILDCARD,$(UNIFY_DIST)/$(SDK_PATH)$(PKG_BASENAME)-$(UNIFY_ARCH).sdk$(SDK_SUFFIX).asc)
+endif
+
+SIGN_CHECKSUM_CMD=
+ifdef MOZ_SIGN_CMD
+ # If we're signing with gpg, we'll have a bunch of extra detached signatures to
+ # upload. We also want to sign our checksums file
+ SIGN_CHECKSUM_CMD=$(MOZ_SIGN_CMD) -f gpg $(CHECKSUM_FILE)
+
+ CHECKSUM_FILES += $(CHECKSUM_FILE).asc
+ UPLOAD_FILES += $(call QUOTED_WILDCARD,$(DIST)/$(COMPLETE_MAR).asc)
+ UPLOAD_FILES += $(call QUOTED_WILDCARD,$(wildcard $(DIST)/$(PARTIAL_MAR).asc))
+ UPLOAD_FILES += $(call QUOTED_WILDCARD,$(INSTALLER_PACKAGE).asc)
+ UPLOAD_FILES += $(call QUOTED_WILDCARD,$(DIST)/$(PACKAGE).asc)
+endif
+
+ifdef MOZ_STUB_INSTALLER
+ UPLOAD_FILES += $(call QUOTED_WILDCARD,$(DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe)
+endif
+
+ifndef MOZ_PKG_SRCDIR
+ MOZ_PKG_SRCDIR = $(topsrcdir)
+endif
+
+SRC_TAR_PREFIX = $(MOZ_APP_NAME)-$(MOZ_PKG_VERSION)
+SRC_TAR_EXCLUDE_PATHS += \
+ --exclude='.hg*' \
+ --exclude='CVS' \
+ --exclude='.cvs*' \
+ --exclude='.mozconfig*' \
+ --exclude='*.pyc' \
+ --exclude='$(MOZILLA_DIR)/Makefile' \
+ --exclude='$(MOZILLA_DIR)/dist'
+ifdef MOZ_OBJDIR
+ SRC_TAR_EXCLUDE_PATHS += --exclude='$(MOZ_OBJDIR)'
+endif
+CREATE_SOURCE_TAR = $(TAR) -c --owner=0 --group=0 --numeric-owner \
+ --mode=go-w $(SRC_TAR_EXCLUDE_PATHS) --transform='s,^\./,$(SRC_TAR_PREFIX)/,' -f
+
+SOURCE_TAR = $(DIST)/$(PKG_SRCPACK_PATH)$(PKG_SRCPACK_BASENAME).tar.xz
+HG_BUNDLE_FILE = $(DIST)/$(PKG_SRCPACK_PATH)$(PKG_BUNDLE_BASENAME).bundle
+SOURCE_CHECKSUM_FILE = $(DIST)/$(PKG_SRCPACK_PATH)$(PKG_SRCPACK_BASENAME).checksums
+SOURCE_UPLOAD_FILES = $(SOURCE_TAR)
+
+HG ?= hg
+CREATE_HG_BUNDLE_CMD = $(HG) -v -R $(topsrcdir) bundle --base null
+ifdef HG_BUNDLE_REVISION
+ CREATE_HG_BUNDLE_CMD += -r $(HG_BUNDLE_REVISION)
+endif
+CREATE_HG_BUNDLE_CMD += $(HG_BUNDLE_FILE)
+ifdef UPLOAD_HG_BUNDLE
+ SOURCE_UPLOAD_FILES += $(HG_BUNDLE_FILE)
+endif
diff --git a/toolkit/mozapps/installer/windows/nsis/common.nsh b/toolkit/mozapps/installer/windows/nsis/common.nsh
new file mode 100755
index 000000000..ac7607449
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/common.nsh
@@ -0,0 +1,8024 @@
+# 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/.
+
+
+################################################################################
+# Helper defines and macros for toolkit applications
+
+/**
+ * Avoid creating macros / functions that overwrite registers (see the
+ * GetLongPath macro for one way to avoid this)!
+ *
+ * Before using the registers exchange the passed in params and save existing
+ * register values to the stack.
+ *
+ * Exch $R9 ; exhange the original $R9 with the top of the stack
+ * Exch 1 ; exchange the top of the stack with 1 below the top of the stack
+ * Exch $R8 ; exchange the original $R8 with the top of the stack
+ * Exch 2 ; exchange the top of the stack with 2 below the top of the stack
+ * Exch $R7 ; exchange the original $R7 with the top of the stack
+ * Push $R6 ; push the original $R6 onto the top of the stack
+ * Push $R5 ; push the original $R5 onto the top of the stack
+ * Push $R4 ; push the original $R4 onto the top of the stack
+ *
+ * <do stuff>
+ *
+ * ; Restore the values.
+ * Pop $R4 ; restore the value for $R4 from the top of the stack
+ * Pop $R5 ; restore the value for $R5 from the top of the stack
+ * Pop $R6 ; restore the value for $R6 from the top of the stack
+ * Exch $R7 ; exchange the new $R7 value with the top of the stack
+ * Exch 2 ; exchange the top of the stack with 2 below the top of the stack
+ * Exch $R8 ; exchange the new $R8 value with the top of the stack
+ * Exch 1 ; exchange the top of the stack with 2 below the top of the stack
+ * Exch $R9 ; exchange the new $R9 value with the top of the stack
+ *
+ *
+ * When inserting macros in common.nsh from another macro in common.nsh that
+ * can be used from the uninstaller _MOZFUNC_UN will be undefined when it is
+ * inserted. Use the following to redefine _MOZFUNC_UN with its original value
+ * (see the RegCleanMain macro for an example).
+ *
+ * !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ * !insertmacro ${_MOZFUNC_UN_TMP}FileJoin
+ * !insertmacro ${_MOZFUNC_UN_TMP}LineFind
+ * !insertmacro ${_MOZFUNC_UN_TMP}TextCompareNoDetails
+ * !insertmacro ${_MOZFUNC_UN_TMP}TrimNewLines
+ * !undef _MOZFUNC_UN
+ * !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ * !undef _MOZFUNC_UN_TMP
+ */
+
+; When including a file provided by NSIS check if its verbose macro is defined
+; to prevent loading the file a second time.
+!ifmacrondef TEXTFUNC_VERBOSE
+ !include TextFunc.nsh
+!endif
+
+!ifmacrondef FILEFUNC_VERBOSE
+ !include FileFunc.nsh
+!endif
+
+!ifmacrondef LOGICLIB_VERBOSITY
+ !include LogicLib.nsh
+!endif
+
+!ifndef WINMESSAGES_INCLUDED
+ !include WinMessages.nsh
+!endif
+
+; When including WinVer.nsh check if ___WINVER__NSH___ is defined to prevent
+; loading the file a second time.
+!ifndef ___WINVER__NSH___
+ !include WinVer.nsh
+!endif
+
+!include x64.nsh
+
+; NSIS provided macros that we have overridden.
+!include overrides.nsh
+
+!define SHORTCUTS_LOG "shortcuts_log.ini"
+!define TO_BE_DELETED "tobedeleted"
+
+; !define SHCNF_DWORD 0x0003
+; !define SHCNF_FLUSH 0x1000
+!ifndef SHCNF_DWORDFLUSH
+ !define SHCNF_DWORDFLUSH 0x1003
+!endif
+!ifndef SHCNE_ASSOCCHANGED
+ !define SHCNE_ASSOCCHANGED 0x08000000
+!endif
+
+################################################################################
+# Macros for debugging
+
+/**
+ * The following two macros assist with verifying that a macro doesn't
+ * overwrite any registers.
+ *
+ * Usage:
+ * ${debugSetRegisters}
+ * <do stuff>
+ * ${debugDisplayRegisters}
+ */
+
+/**
+ * Sets all register values to their name to assist with verifying that a macro
+ * doesn't overwrite any registers.
+ */
+!macro debugSetRegisters
+ StrCpy $0 "$$0"
+ StrCpy $1 "$$1"
+ StrCpy $2 "$$2"
+ StrCpy $3 "$$3"
+ StrCpy $4 "$$4"
+ StrCpy $5 "$$5"
+ StrCpy $6 "$$6"
+ StrCpy $7 "$$7"
+ StrCpy $8 "$$8"
+ StrCpy $9 "$$9"
+ StrCpy $R0 "$$R0"
+ StrCpy $R1 "$$R1"
+ StrCpy $R2 "$$R2"
+ StrCpy $R3 "$$R3"
+ StrCpy $R4 "$$R4"
+ StrCpy $R5 "$$R5"
+ StrCpy $R6 "$$R6"
+ StrCpy $R7 "$$R7"
+ StrCpy $R8 "$$R8"
+ StrCpy $R9 "$$R9"
+!macroend
+!define debugSetRegisters "!insertmacro debugSetRegisters"
+
+/**
+ * Displays all register values to assist with verifying that a macro doesn't
+ * overwrite any registers.
+ */
+!macro debugDisplayRegisters
+ MessageBox MB_OK \
+ "Register Values:$\n\
+ $$0 = $0$\n$$1 = $1$\n$$2 = $2$\n$$3 = $3$\n$$4 = $4$\n\
+ $$5 = $5$\n$$6 = $6$\n$$7 = $7$\n$$8 = $8$\n$$9 = $9$\n\
+ $$R0 = $R0$\n$$R1 = $R1$\n$$R2 = $R2$\n$$R3 = $R3$\n$$R4 = $R4$\n\
+ $$R5 = $R5$\n$$R6 = $R6$\n$$R7 = $R7$\n$$R8 = $R8$\n$$R9 = $R9"
+!macroend
+!define debugDisplayRegisters "!insertmacro debugDisplayRegisters"
+
+
+################################################################################
+# Modern User Interface (MUI) override macros
+
+; Removed macros in nsis 2.33u (ported from nsis 2.22)
+; MUI_LANGUAGEFILE_DEFINE
+; MUI_LANGUAGEFILE_LANGSTRING_PAGE
+; MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE
+; MUI_LANGUAGEFILE_LANGSTRING_DEFINE
+; MUI_LANGUAGEFILE_UNLANGSTRING_PAGE
+
+!macro MOZ_MUI_LANGUAGEFILE_DEFINE DEFINE NAME
+
+ !ifndef "${DEFINE}"
+ !define "${DEFINE}" "${${NAME}}"
+ !endif
+ !undef "${NAME}"
+
+!macroend
+
+!macro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE PAGE NAME
+
+ !ifdef MUI_${PAGE}PAGE
+ LangString "${NAME}" 0 "${${NAME}}"
+ !undef "${NAME}"
+ !else
+ !undef "${NAME}"
+ !endif
+
+!macroend
+
+!macro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE PAGE NAME
+
+ !ifdef MUI_${PAGE}PAGE | MUI_UN${PAGE}PAGE
+ LangString "${NAME}" 0 "${${NAME}}"
+ !undef "${NAME}"
+ !else
+ !undef "${NAME}"
+ !endif
+
+!macroend
+
+!macro MOZ_MUI_LANGUAGEFILE_LANGSTRING_DEFINE DEFINE NAME
+
+ !ifdef "${DEFINE}"
+ LangString "${NAME}" 0 "${${NAME}}"
+ !endif
+ !undef "${NAME}"
+
+!macroend
+
+!macro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE PAGE NAME
+
+ !ifdef MUI_UNINSTALLER
+ !ifdef MUI_UN${PAGE}PAGE
+ LangString "${NAME}" 0 "${${NAME}}"
+ !undef "${NAME}"
+ !else
+ !undef "${NAME}"
+ !endif
+ !else
+ !undef "${NAME}"
+ !endif
+
+!macroend
+
+; Modified version of the following MUI macros to support Mozilla localization.
+; MUI_LANGUAGE
+; MUI_LANGUAGEFILE_BEGIN
+; MOZ_MUI_LANGUAGEFILE_END
+; See <NSIS App Dir>/Contrib/Modern UI/System.nsh for more information
+!define MUI_INSTALLOPTIONS_READ "!insertmacro MUI_INSTALLOPTIONS_READ"
+
+!macro MOZ_MUI_LANGUAGE LANGUAGE
+ !verbose push
+ !verbose ${MUI_VERBOSE}
+ !include "${LANGUAGE}.nsh"
+ !verbose pop
+!macroend
+
+!macro MOZ_MUI_LANGUAGEFILE_BEGIN LANGUAGE
+ !insertmacro MUI_INSERT
+ !ifndef "MUI_LANGUAGEFILE_${LANGUAGE}_USED"
+ !define "MUI_LANGUAGEFILE_${LANGUAGE}_USED"
+ LoadLanguageFile "${LANGUAGE}.nlf"
+ !else
+ !error "Modern UI language file ${LANGUAGE} included twice!"
+ !endif
+!macroend
+
+; Custom version of MUI_LANGUAGEFILE_END. The macro to add the default MUI
+; strings and the macros for several strings that are part of the NSIS MUI and
+; not in our locale files have been commented out.
+!macro MOZ_MUI_LANGUAGEFILE_END
+
+# !include "${NSISDIR}\Contrib\Modern UI\Language files\Default.nsh"
+ !ifdef MUI_LANGUAGEFILE_DEFAULT_USED
+ !undef MUI_LANGUAGEFILE_DEFAULT_USED
+ !warning "${LANGUAGE} Modern UI language file version doesn't match. Using default English texts for missing strings."
+ !endif
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_DEFINE "MUI_${LANGUAGE}_LANGNAME" "MUI_LANGNAME"
+
+ !ifndef MUI_LANGDLL_PUSHLIST
+ !define MUI_LANGDLL_PUSHLIST "'${MUI_${LANGUAGE}_LANGNAME}' ${LANG_${LANGUAGE}} "
+ !else
+ !ifdef MUI_LANGDLL_PUSHLIST_TEMP
+ !undef MUI_LANGDLL_PUSHLIST_TEMP
+ !endif
+ !define MUI_LANGDLL_PUSHLIST_TEMP "${MUI_LANGDLL_PUSHLIST}"
+ !undef MUI_LANGDLL_PUSHLIST
+ !define MUI_LANGDLL_PUSHLIST "'${MUI_${LANGUAGE}_LANGNAME}' ${LANG_${LANGUAGE}} ${MUI_LANGDLL_PUSHLIST_TEMP}"
+ !endif
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE WELCOME "MUI_TEXT_WELCOME_INFO_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE WELCOME "MUI_TEXT_WELCOME_INFO_TEXT"
+
+!ifdef MUI_TEXT_LICENSE_TITLE
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE LICENSE "MUI_TEXT_LICENSE_TITLE"
+!endif
+!ifdef MUI_TEXT_LICENSE_SUBTITLE
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE LICENSE "MUI_TEXT_LICENSE_SUBTITLE"
+!endif
+!ifdef MUI_INNERTEXT_LICENSE_TOP
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE LICENSE "MUI_INNERTEXT_LICENSE_TOP"
+!endif
+
+# !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE LICENSE "MUI_INNERTEXT_LICENSE_BOTTOM"
+
+!ifdef MUI_INNERTEXT_LICENSE_BOTTOM_CHECKBOX
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE LICENSE "MUI_INNERTEXT_LICENSE_BOTTOM_CHECKBOX"
+!endif
+
+!ifdef MUI_INNERTEXT_LICENSE_BOTTOM_RADIOBUTTONS
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE LICENSE "MUI_INNERTEXT_LICENSE_BOTTOM_RADIOBUTTONS"
+!endif
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE COMPONENTS "MUI_TEXT_COMPONENTS_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE COMPONENTS "MUI_TEXT_COMPONENTS_SUBTITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE COMPONENTS "MUI_INNERTEXT_COMPONENTS_DESCRIPTION_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE COMPONENTS "MUI_INNERTEXT_COMPONENTS_DESCRIPTION_INFO"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE DIRECTORY "MUI_TEXT_DIRECTORY_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE DIRECTORY "MUI_TEXT_DIRECTORY_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE STARTMENU "MUI_TEXT_STARTMENU_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE STARTMENU "MUI_TEXT_STARTMENU_SUBTITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE STARTMENU "MUI_INNERTEXT_STARTMENU_TOP"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE STARTMENU "MUI_INNERTEXT_STARTMENU_CHECKBOX"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_INSTALLING_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_INSTALLING_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_FINISH_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_FINISH_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_ABORT_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE INSTFILES "MUI_TEXT_ABORT_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE FINISH "MUI_BUTTONTEXT_FINISH"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_INFO_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_INFO_TEXT"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_INFO_REBOOT"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_REBOOTNOW"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_REBOOTLATER"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_RUN"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_MULTILANGSTRING_PAGE FINISH "MUI_TEXT_FINISH_SHOWREADME"
+
+; Support for using the existing MUI_TEXT_ABORTWARNING string
+!ifdef MOZ_MUI_CUSTOM_ABORT
+ LangString MOZ_MUI_TEXT_ABORTWARNING 0 "${MUI_TEXT_ABORTWARNING}"
+!endif
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_DEFINE MUI_ABORTWARNING "MUI_TEXT_ABORTWARNING"
+
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE WELCOME "MUI_UNTEXT_WELCOME_INFO_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE WELCOME "MUI_UNTEXT_WELCOME_INFO_TEXT"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE CONFIRM "MUI_UNTEXT_CONFIRM_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE CONFIRM "MUI_UNTEXT_CONFIRM_SUBTITLE"
+
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE LICENSE "MUI_UNTEXT_LICENSE_TITLE"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE LICENSE "MUI_UNTEXT_LICENSE_SUBTITLE"
+
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE LICENSE "MUI_UNINNERTEXT_LICENSE_BOTTOM"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE LICENSE "MUI_UNINNERTEXT_LICENSE_BOTTOM_CHECKBOX"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE LICENSE "MUI_UNINNERTEXT_LICENSE_BOTTOM_RADIOBUTTONS"
+
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE COMPONENTS "MUI_UNTEXT_COMPONENTS_TITLE"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE COMPONENTS "MUI_UNTEXT_COMPONENTS_SUBTITLE"
+
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE DIRECTORY "MUI_UNTEXT_DIRECTORY_TITLE"
+# !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE DIRECTORY "MUI_UNTEXT_DIRECTORY_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_UNINSTALLING_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_UNINSTALLING_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_FINISH_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_FINISH_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_ABORT_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE INSTFILES "MUI_UNTEXT_ABORT_SUBTITLE"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE FINISH "MUI_UNTEXT_FINISH_INFO_TITLE"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE FINISH "MUI_UNTEXT_FINISH_INFO_TEXT"
+ !insertmacro MOZ_MUI_LANGUAGEFILE_UNLANGSTRING_PAGE FINISH "MUI_UNTEXT_FINISH_INFO_REBOOT"
+
+ !insertmacro MOZ_MUI_LANGUAGEFILE_LANGSTRING_DEFINE MUI_UNABORTWARNING "MUI_UNTEXT_ABORTWARNING"
+
+ !ifndef MUI_LANGDLL_LANGUAGES
+ !define MUI_LANGDLL_LANGUAGES "'${LANGFILE_${LANGUAGE}_NAME}' '${LANG_${LANGUAGE}}' "
+ !define MUI_LANGDLL_LANGUAGES_CP "'${LANGFILE_${LANGUAGE}_NAME}' '${LANG_${LANGUAGE}}' '${LANG_${LANGUAGE}_CP}' "
+ !else
+ !ifdef MUI_LANGDLL_LANGUAGES_TEMP
+ !undef MUI_LANGDLL_LANGUAGES_TEMP
+ !endif
+ !define MUI_LANGDLL_LANGUAGES_TEMP "${MUI_LANGDLL_LANGUAGES}"
+ !undef MUI_LANGDLL_LANGUAGES
+
+ !ifdef MUI_LANGDLL_LANGUAGES_CP_TEMP
+ !undef MUI_LANGDLL_LANGUAGES_CP_TEMP
+ !endif
+ !define MUI_LANGDLL_LANGUAGES_CP_TEMP "${MUI_LANGDLL_LANGUAGES_CP}"
+ !undef MUI_LANGDLL_LANGUAGES_CP
+
+ !define MUI_LANGDLL_LANGUAGES "'${LANGFILE_${LANGUAGE}_NAME}' '${LANG_${LANGUAGE}}' ${MUI_LANGDLL_LANGUAGES_TEMP}"
+ !define MUI_LANGDLL_LANGUAGES_CP "'${LANGFILE_${LANGUAGE}_NAME}' '${LANG_${LANGUAGE}}' '${LANG_${LANGUAGE}_CP}' ${MUI_LANGDLL_LANGUAGES_CP_TEMP}"
+ !endif
+
+!macroend
+
+/**
+ * Creates an InstallOptions file with a UTF-16LE BOM and adds the RTL value
+ * to the Settings section.
+ *
+ * @param _FILE
+ * The name of the file to be created in $PLUGINSDIR.
+ */
+!macro InitInstallOptionsFile _FILE
+ Push $R9
+
+ FileOpen $R9 "$PLUGINSDIR\${_FILE}" w
+ FileWriteWord $R9 "65279"
+ FileClose $R9
+ WriteIniStr "$PLUGINSDIR\${_FILE}" "Settings" "RTL" "$(^RTL)"
+
+ Pop $R9
+!macroend
+
+
+################################################################################
+# Macros for handling files in use
+
+/**
+ * Checks for files in use in the $INSTDIR directory. To check files in
+ * sub-directories this macro would need to be rewritten to create
+ * sub-directories in the temporary directory used to backup the files that are
+ * checked.
+ *
+ * Example usage:
+ *
+ * ; The first string to be pushed onto the stack MUST be "end" to indicate
+ * ; that there are no more files in the $INSTDIR directory to check.
+ * Push "end"
+ * Push "freebl3.dll"
+ * ; The last file pushed should be the app's main exe so if it is in use this
+ * ; macro will return after the first check.
+ * Push "${FileMainEXE}"
+ * ${CheckForFilesInUse} $R9
+ *
+ * !IMPORTANT - this macro uses the $R7, $R8, and $R9 registers and makes no
+ * attempt to restore their original values.
+ *
+ * @return _RESULT
+ * false if all of the files popped from the stack are not in use.
+ * True if any of the files popped from the stack are in use.
+ * $R7 = Temporary backup directory where the files will be copied to.
+ * $R8 = value popped from the stack. This will either be a file name for a file
+ * in the $INSTDIR directory or "end" to indicate that there are no
+ * additional files to check.
+ * $R9 = _RESULT
+ */
+!macro CheckForFilesInUse
+
+ !ifndef ${_MOZFUNC_UN}CheckForFilesInUse
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CheckForFilesInUse "!insertmacro ${_MOZFUNC_UN}CheckForFilesInUseCall"
+
+ Function ${_MOZFUNC_UN}CheckForFilesInUse
+ ; Create a temporary backup directory.
+ GetTempFileName $R7 "$INSTDIR"
+ Delete "$R7"
+ SetOutPath "$R7"
+ StrCpy $R9 "false"
+
+ Pop $R8
+ ${While} $R8 != "end"
+ ${Unless} ${FileExists} "$INSTDIR\$R8"
+ Pop $R8 ; get next file to check before continuing
+ ${Continue}
+ ${EndUnless}
+
+ ClearErrors
+ CopyFiles /SILENT "$INSTDIR\$R8" "$R7\$R8" ; try to copy
+ ${If} ${Errors}
+ ; File is in use
+ StrCpy $R9 "true"
+ ${Break}
+ ${EndIf}
+
+ Delete "$INSTDIR\$R8" ; delete original
+ ${If} ${Errors}
+ ; File is in use
+ StrCpy $R9 "true"
+ Delete "$R7\$R8" ; delete temp copy
+ ${Break}
+ ${EndIf}
+
+ Pop $R8 ; get next file to check
+ ${EndWhile}
+
+ ; clear stack
+ ${While} $R8 != "end"
+ Pop $R8
+ ${EndWhile}
+
+ ; restore everything
+ SetOutPath "$INSTDIR"
+ CopyFiles /SILENT "$R7\*" "$INSTDIR\"
+ RmDir /r "$R7"
+ SetOutPath "$EXEDIR"
+ ClearErrors
+
+ Push $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CheckForFilesInUseCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call CheckForFilesInUse
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CheckForFilesInUseCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.CheckForFilesInUse
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CheckForFilesInUse
+ !ifndef un.CheckForFilesInUse
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CheckForFilesInUse
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * The macros below will automatically prepend un. to the function names when
+ * they are defined (e.g. !define un.RegCleanMain).
+ */
+!verbose push
+!verbose 3
+!ifndef _MOZFUNC_VERBOSE
+ !define _MOZFUNC_VERBOSE 3
+!endif
+!verbose ${_MOZFUNC_VERBOSE}
+!define MOZFUNC_VERBOSE "!insertmacro MOZFUNC_VERBOSE"
+!define _MOZFUNC_UN
+!define _MOZFUNC_S
+!verbose pop
+
+!macro MOZFUNC_VERBOSE _VERBOSE
+ !verbose push
+ !verbose 3
+ !undef _MOZFUNC_VERBOSE
+ !define _MOZFUNC_VERBOSE ${_VERBOSE}
+ !verbose pop
+!macroend
+
+/**
+ * Displays a MessageBox and then calls abort to prevent continuing to the
+ * next page when the specified Window Class is found.
+ *
+ * @param _WINDOW_CLASS
+ * The Window Class to search for with FindWindow.
+ * @param _MSG
+ * The message text to display in the message box.
+ *
+ * $R7 = return value from FindWindow
+ * $R8 = _WINDOW_CLASS
+ * $R9 = _MSG
+ */
+!macro ManualCloseAppPrompt
+
+ !ifndef ${_MOZFUNC_UN}ManualCloseAppPrompt
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}ManualCloseAppPrompt "!insertmacro ${_MOZFUNC_UN}ManualCloseAppPromptCall"
+
+ Function ${_MOZFUNC_UN}ManualCloseAppPrompt
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Push $R7
+
+ FindWindow $R7 "$R8"
+ ${If} $R7 <> 0 ; integer comparison
+ MessageBox MB_OK|MB_ICONQUESTION "$R9"
+ Abort
+ ${EndIf}
+
+ Pop $R7
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro ManualCloseAppPromptCall _WINDOW_CLASS _MSG
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_WINDOW_CLASS}"
+ Push "${_MSG}"
+ Call ManualCloseAppPrompt
+ !verbose pop
+!macroend
+
+!macro un.ManualCloseAppPromptCall _WINDOW_CLASS _MSG
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_WINDOW_CLASS}"
+ Push "${_MSG}"
+ Call un.ManualCloseAppPrompt
+ !verbose pop
+!macroend
+
+!macro un.ManualCloseAppPrompt
+ !ifndef un.ManualCloseAppPrompt
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro ManualCloseAppPrompt
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for working with the registry
+
+/**
+ * Writes a registry string using SHCTX and the supplied params and logs the
+ * action to the install log and the uninstall log if _LOG_UNINSTALL equals 1.
+ *
+ * Define NO_LOG to prevent all logging when calling this from the uninstaller.
+ *
+ * @param _ROOT
+ * The registry key root as defined by NSIS (e.g. HKLM, HKCU, etc.).
+ * This will only be used for logging.
+ * @param _KEY
+ * The subkey in relation to the key root.
+ * @param _NAME
+ * The key value name to write to.
+ * @param _STR
+ * The string to write to the key value name.
+ * @param _LOG_UNINSTALL
+ * 0 = don't add to uninstall log, 1 = add to uninstall log.
+ *
+ * $R5 = _ROOT
+ * $R6 = _KEY
+ * $R7 = _NAME
+ * $R8 = _STR
+ * $R9 = _LOG_UNINSTALL
+ */
+!macro WriteRegStr2
+
+ !ifndef ${_MOZFUNC_UN}WriteRegStr2
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}WriteRegStr2 "!insertmacro ${_MOZFUNC_UN}WriteRegStr2Call"
+
+ Function ${_MOZFUNC_UN}WriteRegStr2
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Exch 4
+ Exch $R5
+
+ ClearErrors
+ WriteRegStr SHCTX "$R6" "$R7" "$R8"
+
+ !ifndef NO_LOG
+ ${If} ${Errors}
+ ${LogMsg} "** ERROR Adding Registry String: $R5 | $R6 | $R7 | $R8 **"
+ ${Else}
+ ${If} $R9 == 1 ; add to the uninstall log?
+ ${LogUninstall} "RegVal: $R5 | $R6 | $R7"
+ ${EndIf}
+ ${LogMsg} "Added Registry String: $R5 | $R6 | $R7 | $R8"
+ ${EndIf}
+ !endif
+
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro WriteRegStr2Call _ROOT _KEY _NAME _STR _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_STR}"
+ Push "${_LOG_UNINSTALL}"
+ Call WriteRegStr2
+ !verbose pop
+!macroend
+
+!macro un.WriteRegStr2Call _ROOT _KEY _NAME _STR _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_STR}"
+ Push "${_LOG_UNINSTALL}"
+ Call un.WriteRegStr2
+ !verbose pop
+!macroend
+
+!macro un.WriteRegStr2
+ !ifndef un.WriteRegStr2
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro WriteRegStr2
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Writes a registry dword using SHCTX and the supplied params and logs the
+ * action to the install log and the uninstall log if _LOG_UNINSTALL equals 1.
+ *
+ * Define NO_LOG to prevent all logging when calling this from the uninstaller.
+ *
+ * @param _ROOT
+ * The registry key root as defined by NSIS (e.g. HKLM, HKCU, etc.).
+ * This will only be used for logging.
+ * @param _KEY
+ * The subkey in relation to the key root.
+ * @param _NAME
+ * The key value name to write to.
+ * @param _DWORD
+ * The dword to write to the key value name.
+ * @param _LOG_UNINSTALL
+ * 0 = don't add to uninstall log, 1 = add to uninstall log.
+ *
+ * $R5 = _ROOT
+ * $R6 = _KEY
+ * $R7 = _NAME
+ * $R8 = _DWORD
+ * $R9 = _LOG_UNINSTALL
+ */
+!macro WriteRegDWORD2
+
+ !ifndef ${_MOZFUNC_UN}WriteRegDWORD2
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}WriteRegDWORD2 "!insertmacro ${_MOZFUNC_UN}WriteRegDWORD2Call"
+
+ Function ${_MOZFUNC_UN}WriteRegDWORD2
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Exch 4
+ Exch $R5
+
+ ClearErrors
+ WriteRegDWORD SHCTX "$R6" "$R7" "$R8"
+
+ !ifndef NO_LOG
+ ${If} ${Errors}
+ ${LogMsg} "** ERROR Adding Registry DWord: $R5 | $R6 | $R7 | $R8 **"
+ ${Else}
+ ${If} $R9 == 1 ; add to the uninstall log?
+ ${LogUninstall} "RegVal: $R5 | $R6 | $R7"
+ ${EndIf}
+ ${LogMsg} "Added Registry DWord: $R5 | $R6 | $R7 | $R8"
+ ${EndIf}
+ !endif
+
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro WriteRegDWORD2Call _ROOT _KEY _NAME _DWORD _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_DWORD}"
+ Push "${_LOG_UNINSTALL}"
+ Call WriteRegDWORD2
+ !verbose pop
+!macroend
+
+!macro un.WriteRegDWORD2Call _ROOT _KEY _NAME _DWORD _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_DWORD}"
+ Push "${_LOG_UNINSTALL}"
+ Call un.WriteRegDWORD2
+ !verbose pop
+!macroend
+
+!macro un.WriteRegDWORD2
+ !ifndef un.WriteRegDWORD2
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro WriteRegDWORD2
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Writes a registry string to HKCR using the supplied params and logs the
+ * action to the install log and the uninstall log if _LOG_UNINSTALL equals 1.
+ *
+ * Define NO_LOG to prevent all logging when calling this from the uninstaller.
+ *
+ * @param _ROOT
+ * The registry key root as defined by NSIS (e.g. HKLM, HKCU, etc.).
+ * This will only be used for logging.
+ * @param _KEY
+ * The subkey in relation to the key root.
+ * @param _NAME
+ * The key value name to write to.
+ * @param _STR
+ * The string to write to the key value name.
+ * @param _LOG_UNINSTALL
+ * 0 = don't add to uninstall log, 1 = add to uninstall log.
+ *
+ * $R5 = _ROOT
+ * $R6 = _KEY
+ * $R7 = _NAME
+ * $R8 = _STR
+ * $R9 = _LOG_UNINSTALL
+ */
+!macro WriteRegStrHKCR
+
+ !ifndef ${_MOZFUNC_UN}WriteRegStrHKCR
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}WriteRegStrHKCR "!insertmacro ${_MOZFUNC_UN}WriteRegStrHKCRCall"
+
+ Function ${_MOZFUNC_UN}WriteRegStrHKCR
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Exch 4
+ Exch $R5
+
+ ClearErrors
+ WriteRegStr HKCR "$R6" "$R7" "$R8"
+
+ !ifndef NO_LOG
+ ${If} ${Errors}
+ ${LogMsg} "** ERROR Adding Registry String: $R5 | $R6 | $R7 | $R8 **"
+ ${Else}
+ ${If} $R9 == 1 ; add to the uninstall log?
+ ${LogUninstall} "RegVal: $R5 | $R6 | $R7"
+ ${EndIf}
+ ${LogMsg} "Added Registry String: $R5 | $R6 | $R7 | $R8"
+ ${EndIf}
+ !endif
+
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro WriteRegStrHKCRCall _ROOT _KEY _NAME _STR _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_STR}"
+ Push "${_LOG_UNINSTALL}"
+ Call WriteRegStrHKCR
+ !verbose pop
+!macroend
+
+!macro un.WriteRegStrHKCRCall _ROOT _KEY _NAME _STR _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_NAME}"
+ Push "${_STR}"
+ Push "${_LOG_UNINSTALL}"
+ Call un.WriteRegStrHKCR
+ !verbose pop
+!macroend
+
+!macro un.WriteRegStrHKCR
+ !ifndef un.WriteRegStrHKCR
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro WriteRegStrHKCR
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+!ifndef KEY_SET_VALUE
+ !define KEY_SET_VALUE 0x0002
+!endif
+!ifndef KEY_WOW64_64KEY
+ !define KEY_WOW64_64KEY 0x0100
+!endif
+!ifndef HAVE_64BIT_BUILD
+ !define CREATE_KEY_SAM ${KEY_SET_VALUE}
+!else
+ !define CREATE_KEY_SAM ${KEY_SET_VALUE}|${KEY_WOW64_64KEY}
+!endif
+
+/**
+ * Creates a registry key. This will log the actions to the install and
+ * uninstall logs. Alternatively you can set a registry value to create the key
+ * and then delete the value.
+ *
+ * Define NO_LOG to prevent all logging when calling this from the uninstaller.
+ *
+ * @param _ROOT
+ * The registry key root as defined by NSIS (e.g. HKLM, HKCU, etc.).
+ * @param _KEY
+ * The subkey in relation to the key root.
+ * @param _LOG_UNINSTALL
+ * 0 = don't add to uninstall log, 1 = add to uninstall log.
+ *
+ * $R4 = [out] handle to newly created registry key. If this is not a key
+ * located in one of the predefined registry keys this must be closed
+ * with RegCloseKey (this should not be needed unless someone decides to
+ * do something extremely squirrelly with NSIS).
+ * $R5 = return value from RegCreateKeyExW (represented by R5 in the system call).
+ * $R6 = [in] hKey passed to RegCreateKeyExW.
+ * $R7 = _ROOT
+ * $R8 = _KEY
+ * $R9 = _LOG_UNINSTALL
+ */
+!macro CreateRegKey
+
+ !ifndef ${_MOZFUNC_UN}CreateRegKey
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CreateRegKey "!insertmacro ${_MOZFUNC_UN}CreateRegKeyCall"
+
+ Function ${_MOZFUNC_UN}CreateRegKey
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Push $R6
+ Push $R5
+ Push $R4
+
+ StrCmp $R7 "HKCR" +1 +2
+ StrCpy $R6 "0x80000000"
+ StrCmp $R7 "HKCU" +1 +2
+ StrCpy $R6 "0x80000001"
+ StrCmp $R7 "HKLM" +1 +2
+ StrCpy $R6 "0x80000002"
+
+ ; see definition of RegCreateKey
+ System::Call "Advapi32::RegCreateKeyExW(i R6, w R8, i 0, i 0, i 0,\
+ i ${CREATE_KEY_SAM}, i 0, *i .R4,\
+ i 0) i .R5"
+
+ !ifndef NO_LOG
+ ; if $R5 is not 0 then there was an error creating the registry key.
+ ${If} $R5 <> 0
+ ${LogMsg} "** ERROR Adding Registry Key: $R7 | $R8 **"
+ ${Else}
+ ${If} $R9 == 1 ; add to the uninstall log?
+ ${LogUninstall} "RegKey: $R7 | $R8"
+ ${EndIf}
+ ${LogMsg} "Added Registry Key: $R7 | $R8"
+ ${EndIf}
+ !endif
+
+ StrCmp $R5 0 +1 +2
+ System::Call "Advapi32::RegCloseKey(iR4)"
+
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CreateRegKeyCall _ROOT _KEY _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_LOG_UNINSTALL}"
+ Call CreateRegKey
+ !verbose pop
+!macroend
+
+!macro un.CreateRegKeyCall _ROOT _KEY _LOG_UNINSTALL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_ROOT}"
+ Push "${_KEY}"
+ Push "${_LOG_UNINSTALL}"
+ Call un.CreateRegKey
+ !verbose pop
+!macroend
+
+!macro un.CreateRegKey
+ !ifndef un.CreateRegKey
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CreateRegKey
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Helper for checking for the existence of a registry key.
+ * SHCTX is the root key to search.
+ *
+ * @param _MAIN_KEY
+ * Sub key to iterate for the key in question
+ * @param _KEY
+ * Key name to search for
+ * @return _RESULT
+ * 'true' / 'false' result
+ */
+!macro CheckIfRegistryKeyExists
+ !ifndef CheckIfRegistryKeyExists
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define CheckIfRegistryKeyExists "!insertmacro CheckIfRegistryKeyExistsCall"
+
+ Function CheckIfRegistryKeyExists
+ ; stack: main key, key
+ Exch $R9 ; main key, stack: old R9, key
+ Exch 1 ; stack: key, old R9
+ Exch $R8 ; key, stack: old R8, old R9
+ Push $R7
+ Push $R6
+ Push $R5
+
+ StrCpy $R5 "false"
+ StrCpy $R7 "0" # loop index
+ ${Do}
+ EnumRegKey $R6 SHCTX "$R9" "$R7"
+ ${If} "$R6" == "$R8"
+ StrCpy $R5 "true"
+ ${Break}
+ ${EndIf}
+ IntOp $R7 $R7 + 1
+ ${LoopWhile} $R6 != ""
+ ClearErrors
+
+ StrCpy $R9 $R5
+
+ Pop $R5
+ Pop $R6
+ Pop $R7 ; stack: old R8, old R9
+ Pop $R8 ; stack: old R9
+ Exch $R9 ; stack: result
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CheckIfRegistryKeyExistsCall _MAIN_KEY _KEY _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_MAIN_KEY}"
+ Call CheckIfRegistryKeyExists
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+################################################################################
+# Macros for adding file and protocol handlers
+
+/**
+ * Writes common registry values for a handler using SHCTX.
+ *
+ * @param _KEY
+ * The subkey in relation to the key root.
+ * @param _VALOPEN
+ * The path and args to launch the application.
+ * @param _VALICON
+ * The path to the binary that contains the icon group for the default icon
+ * followed by a comma and either the icon group's resource index or the icon
+ * group's resource id prefixed with a minus sign
+ * @param _DISPNAME
+ * The display name for the handler. If emtpy no value will be set.
+ * @param _ISPROTOCOL
+ * Sets protocol handler specific registry values when "true".
+ * Deletes protocol handler specific registry values when "delete".
+ * Otherwise doesn't touch handler specific registry values.
+ * @param _ISDDE
+ * Sets DDE specific registry values when "true".
+ *
+ * $R3 = string value of the current registry key path.
+ * $R4 = _KEY
+ * $R5 = _VALOPEN
+ * $R6 = _VALICON
+ * $R7 = _DISPNAME
+ * $R8 = _ISPROTOCOL
+ * $R9 = _ISDDE
+ */
+!macro AddHandlerValues
+
+ !ifndef ${_MOZFUNC_UN}AddHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}AddHandlerValues "!insertmacro ${_MOZFUNC_UN}AddHandlerValuesCall"
+
+ Function ${_MOZFUNC_UN}AddHandlerValues
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Exch 4
+ Exch $R5
+ Exch 5
+ Exch $R4
+ Push $R3
+
+ StrCmp "$R7" "" +6 +1
+ ReadRegStr $R3 SHCTX "$R4" "FriendlyTypeName"
+
+ StrCmp "$R3" "" +1 +3
+ WriteRegStr SHCTX "$R4" "" "$R7"
+ WriteRegStr SHCTX "$R4" "FriendlyTypeName" "$R7"
+
+ StrCmp "$R8" "true" +1 +2
+ WriteRegStr SHCTX "$R4" "URL Protocol" ""
+ StrCmp "$R8" "delete" +1 +2
+ DeleteRegValue SHCTX "$R4" "URL Protocol"
+ StrCpy $R3 ""
+ ReadRegDWord $R3 SHCTX "$R4" "EditFlags"
+ StrCmp $R3 "" +1 +3 ; Only add EditFlags if a value doesn't exist
+ DeleteRegValue SHCTX "$R4" "EditFlags"
+ WriteRegDWord SHCTX "$R4" "EditFlags" 0x00000002
+
+ StrCmp "$R6" "" +2 +1
+ WriteRegStr SHCTX "$R4\DefaultIcon" "" "$R6"
+
+ StrCmp "$R5" "" +2 +1
+ WriteRegStr SHCTX "$R4\shell\open\command" "" "$R5"
+
+!ifdef DDEApplication
+ StrCmp "$R9" "true" +1 +11
+ WriteRegStr SHCTX "$R4\shell\open\ddeexec" "" "$\"%1$\",,0,0,,,,"
+ WriteRegStr SHCTX "$R4\shell\open\ddeexec" "NoActivateHandler" ""
+ WriteRegStr SHCTX "$R4\shell\open\ddeexec\Application" "" "${DDEApplication}"
+ WriteRegStr SHCTX "$R4\shell\open\ddeexec\Topic" "" "WWW_OpenURL"
+ ; The ifexec key may have been added by another application so try to
+ ; delete it to prevent it from breaking this app's shell integration.
+ ; Also, IE 6 and below doesn't remove this key when it sets itself as the
+ ; default handler and if this key exists IE's shell integration breaks.
+ DeleteRegKey HKLM "$R4\shell\open\ddeexec\ifexec"
+ DeleteRegKey HKCU "$R4\shell\open\ddeexec\ifexec"
+!endif
+
+ ClearErrors
+
+ Pop $R3
+ Exch $R4
+ Exch 5
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro AddHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL _ISDDE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Push "${_ISDDE}"
+ Call AddHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL _ISDDE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Push "${_ISDDE}"
+ Call un.AddHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddHandlerValues
+ !ifndef un.AddHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro AddHandlerValues
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Writes common registry values for a handler that uses DDE using SHCTX.
+ *
+ * @param _KEY
+ * The key name in relation to the HKCR root. SOFTWARE\Classes is
+ * prefixed to this value when using SHCTX.
+ * @param _VALOPEN
+ * The path and args to launch the application.
+ * @param _VALICON
+ * The path to the binary that contains the icon group for the default icon
+ * followed by a comma and either the icon group's resource index or the icon
+ * group's resource id prefixed with a minus sign
+ * @param _DISPNAME
+ * The display name for the handler. If emtpy no value will be set.
+ * @param _ISPROTOCOL
+ * Sets protocol handler specific registry values when "true".
+ * Deletes protocol handler specific registry values when "delete".
+ * Otherwise doesn't touch handler specific registry values.
+ * @param _DDE_APPNAME
+ * Sets DDE specific registry values when not an empty string.
+ *
+ * $R0 = storage for SOFTWARE\Classes
+ * $R1 = string value of the current registry key path.
+ * $R2 = _KEY
+ * $R3 = _VALOPEN
+ * $R4 = _VALICON
+ * $R5 = _DISPNAME
+ * $R6 = _ISPROTOCOL
+ * $R7 = _DDE_APPNAME
+ * $R8 = _DDE_DEFAULT
+ * $R9 = _DDE_TOPIC
+ */
+!macro AddDDEHandlerValues
+
+ !ifndef ${_MOZFUNC_UN}AddDDEHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}AddDDEHandlerValues "!insertmacro ${_MOZFUNC_UN}AddDDEHandlerValuesCall"
+
+ Function ${_MOZFUNC_UN}AddDDEHandlerValues
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Exch 4
+ Exch $R5
+ Exch 5
+ Exch $R4
+ Exch 6
+ Exch $R3
+ Exch 7
+ Exch $R2
+ Push $R1
+ Push $R0
+
+ StrCpy $R0 "SOFTWARE\Classes"
+ StrCmp "$R5" "" +6 +1
+ ReadRegStr $R1 SHCTX "$R2" "FriendlyTypeName"
+
+ StrCmp "$R1" "" +1 +3
+ WriteRegStr SHCTX "$R0\$R2" "" "$R5"
+ WriteRegStr SHCTX "$R0\$R2" "FriendlyTypeName" "$R5"
+
+ StrCmp "$R6" "true" +1 +2
+ WriteRegStr SHCTX "$R0\$R2" "URL Protocol" ""
+ StrCmp "$R6" "delete" +1 +2
+ DeleteRegValue SHCTX "$R0\$R2" "URL Protocol"
+ StrCpy $R1 ""
+ ReadRegDWord $R1 SHCTX "$R0\$R2" "EditFlags"
+ StrCmp $R1 "" +1 +3 ; Only add EditFlags if a value doesn't exist
+ DeleteRegValue SHCTX "$R0\$R2" "EditFlags"
+ WriteRegDWord SHCTX "$R0\$R2" "EditFlags" 0x00000002
+
+ StrCmp "$R4" "" +2 +1
+ WriteRegStr SHCTX "$R0\$R2\DefaultIcon" "" "$R4"
+
+ WriteRegStr SHCTX "$R0\$R2\shell" "" "open"
+ WriteRegStr SHCTX "$R0\$R2\shell\open\command" "" "$R3"
+
+ WriteRegStr SHCTX "$R0\$R2\shell\open\ddeexec" "" "$R8"
+ WriteRegStr SHCTX "$R0\$R2\shell\open\ddeexec" "NoActivateHandler" ""
+ WriteRegStr SHCTX "$R0\$R2\shell\open\ddeexec\Application" "" "$R7"
+ WriteRegStr SHCTX "$R0\$R2\shell\open\ddeexec\Topic" "" "$R9"
+
+ ; The ifexec key may have been added by another application so try to
+ ; delete it to prevent it from breaking this app's shell integration.
+ ; Also, IE 6 and below doesn't remove this key when it sets itself as the
+ ; default handler and if this key exists IE's shell integration breaks.
+ DeleteRegKey HKLM "$R0\$R2\shell\open\ddeexec\ifexec"
+ DeleteRegKey HKCU "$R0\$R2\shell\open\ddeexec\ifexec"
+ ClearErrors
+
+ Pop $R0
+ Pop $R1
+ Exch $R2
+ Exch 7
+ Exch $R3
+ Exch 6
+ Exch $R4
+ Exch 5
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro AddDDEHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL _DDE_APPNAME _DDE_DEFAULT _DDE_TOPIC
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Push "${_DDE_APPNAME}"
+ Push "${_DDE_DEFAULT}"
+ Push "${_DDE_TOPIC}"
+ Call AddDDEHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddDDEHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL _DDE_APPNAME _DDE_DEFAULT _DDE_TOPIC
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Push "${_DDE_APPNAME}"
+ Push "${_DDE_DEFAULT}"
+ Push "${_DDE_TOPIC}"
+ Call un.AddDDEHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddDDEHandlerValues
+ !ifndef un.AddDDEHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro AddDDEHandlerValues
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Writes common registry values for a handler that DOES NOT use DDE using SHCTX.
+ *
+ * @param _KEY
+ * The key name in relation to the HKCR root. SOFTWARE\Classes is
+ * prefixed to this value when using SHCTX.
+ * @param _VALOPEN
+ * The path and args to launch the application.
+ * @param _VALICON
+ * The path to the binary that contains the icon group for the default icon
+ * followed by a comma and either the icon group's resource index or the icon
+ * group's resource id prefixed with a minus sign
+ * @param _DISPNAME
+ * The display name for the handler. If emtpy no value will be set.
+ * @param _ISPROTOCOL
+ * Sets protocol handler specific registry values when "true".
+ * Deletes protocol handler specific registry values when "delete".
+ * Otherwise doesn't touch handler specific registry values.
+ *
+ * $R3 = storage for SOFTWARE\Classes
+ * $R4 = string value of the current registry key path.
+ * $R5 = _KEY
+ * $R6 = _VALOPEN
+ * $R7 = _VALICON
+ * $R8 = _DISPNAME
+ * $R9 = _ISPROTOCOL
+ */
+!macro AddDisabledDDEHandlerValues
+
+ !ifndef ${_MOZFUNC_UN}AddDisabledDDEHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}AddDisabledDDEHandlerValues "!insertmacro ${_MOZFUNC_UN}AddDisabledDDEHandlerValuesCall"
+
+ Function ${_MOZFUNC_UN}AddDisabledDDEHandlerValues
+ Exch $R9 ; _ISPROTOCOL
+ Exch 1
+ Exch $R8 ; FriendlyTypeName
+ Exch 2
+ Exch $R7 ; icon index
+ Exch 3
+ Exch $R6 ; shell\open\command
+ Exch 4
+ Exch $R5 ; reg key
+ Push $R4 ;
+ Push $R3 ; base reg class
+
+ StrCpy $R3 "SOFTWARE\Classes"
+ StrCmp "$R8" "" +6 +1
+ ReadRegStr $R4 SHCTX "$R5" "FriendlyTypeName"
+
+ StrCmp "$R4" "" +1 +3
+ WriteRegStr SHCTX "$R3\$R5" "" "$R8"
+ WriteRegStr SHCTX "$R3\$R5" "FriendlyTypeName" "$R8"
+
+ StrCmp "$R9" "true" +1 +2
+ WriteRegStr SHCTX "$R3\$R5" "URL Protocol" ""
+ StrCmp "$R9" "delete" +1 +2
+ DeleteRegValue SHCTX "$R3\$R5" "URL Protocol"
+ StrCpy $R4 ""
+ ReadRegDWord $R4 SHCTX "$R3\$R5" "EditFlags"
+ StrCmp $R4 "" +1 +3 ; Only add EditFlags if a value doesn't exist
+ DeleteRegValue SHCTX "$R3\$R5" "EditFlags"
+ WriteRegDWord SHCTX "$R3\$R5" "EditFlags" 0x00000002
+
+ StrCmp "$R7" "" +2 +1
+ WriteRegStr SHCTX "$R3\$R5\DefaultIcon" "" "$R7"
+
+ ; Main command handler for the app
+ WriteRegStr SHCTX "$R3\$R5\shell" "" "open"
+ WriteRegStr SHCTX "$R3\$R5\shell\open\command" "" "$R6"
+
+ ; Drop support for DDE (bug 491947), and remove old dde entries if
+ ; they exist.
+ ;
+ ; Note, changes in SHCTX should propegate to hkey classes root when
+ ; current user or local machine entries are written. Windows will also
+ ; attempt to propegate entries when a handler is used. CR entries are a
+ ; combination of LM and CU, with CU taking priority.
+ ;
+ ; To disable dde, an empty shell/ddeexec key must be created in current
+ ; user or local machine. Unfortunately, settings have various different
+ ; behaviors depending on the windows version. The following code attempts
+ ; to address these differences.
+ ;
+ ; On XP (no SP, SP1, SP2), Vista: An empty default string
+ ; must be set under ddeexec. Empty strings propagate to CR.
+ ;
+ ; Win7: IE does not configure ddeexec, so issues with left over ddeexec keys
+ ; in LM are reduced. We configure an empty ddeexec key with an empty default
+ ; string in CU to be sure.
+ ;
+ DeleteRegKey SHCTX "SOFTWARE\Classes\$R5\shell\open\ddeexec"
+ WriteRegStr SHCTX "SOFTWARE\Classes\$R5\shell\open\ddeexec" "" ""
+
+ ClearErrors
+
+ Pop $R3
+ Pop $R4
+ Exch $R5
+ Exch 4
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro AddDisabledDDEHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Call AddDisabledDDEHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddDisabledDDEHandlerValuesCall _KEY _VALOPEN _VALICON _DISPNAME _ISPROTOCOL
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Push "${_VALOPEN}"
+ Push "${_VALICON}"
+ Push "${_DISPNAME}"
+ Push "${_ISPROTOCOL}"
+ Call un.AddDisabledDDEHandlerValues
+ !verbose pop
+!macroend
+
+!macro un.AddDisabledDDEHandlerValues
+ !ifndef un.AddDisabledDDEHandlerValues
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro AddDisabledDDEHandlerValues
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for handling DLL registration
+
+!macro RegisterDLL DLL
+
+ ; The x64 regsvr32.exe registers x86 DLL's properly on Windows Vista and above
+ ; (not on Windows XP http://support.microsoft.com/kb/282747) so just use it
+ ; when installing on an x64 systems even when installing an x86 application.
+ ${If} ${RunningX64}
+ ${DisableX64FSRedirection}
+ ExecWait '"$SYSDIR\regsvr32.exe" /s "${DLL}"'
+ ${EnableX64FSRedirection}
+ ${Else}
+ RegDLL "${DLL}"
+ ${EndIf}
+
+!macroend
+
+!macro UnregisterDLL DLL
+
+ ; The x64 regsvr32.exe registers x86 DLL's properly on Windows Vista and above
+ ; (not on Windows XP http://support.microsoft.com/kb/282747) so just use it
+ ; when installing on an x64 systems even when installing an x86 application.
+ ${If} ${RunningX64}
+ ${DisableX64FSRedirection}
+ ExecWait '"$SYSDIR\regsvr32.exe" /s /u "${DLL}"'
+ ${EnableX64FSRedirection}
+ ${Else}
+ UnRegDLL "${DLL}"
+ ${EndIf}
+
+!macroend
+
+!define RegisterDLL "!insertmacro RegisterDLL"
+!define UnregisterDLL "!insertmacro UnregisterDLL"
+
+
+################################################################################
+# Macros for retrieving existing install paths
+
+/**
+ * Finds a second installation of the application so we can make informed
+ * decisions about registry operations. This uses SHCTX to determine the
+ * registry hive so you must call SetShellVarContext first.
+ *
+ * @param _KEY
+ * The registry subkey (typically this will be Software\Mozilla).
+ * @return _RESULT
+ * false if a second install isn't found, path to the main exe if a
+ * second install is found.
+ *
+ * $R3 = stores the long path to $INSTDIR
+ * $R4 = counter for the outer loop's EnumRegKey
+ * $R5 = return value from ReadRegStr and RemoveQuotesFromPath
+ * $R6 = return value from GetParent
+ * $R7 = return value from the loop's EnumRegKey
+ * $R8 = storage for _KEY
+ * $R9 = _KEY and _RESULT
+ */
+!macro GetSecondInstallPath
+
+ !ifndef ${_MOZFUNC_UN}GetSecondInstallPath
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}RemoveQuotesFromPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}GetSecondInstallPath "!insertmacro ${_MOZFUNC_UN}GetSecondInstallPathCall"
+
+ Function ${_MOZFUNC_UN}GetSecondInstallPath
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R3
+
+ StrCpy $R4 0 ; set the counter for the loop to 0
+ StrCpy $R8 "$R9" ; Registry key path to search
+ StrCpy $R9 "false" ; default return value
+
+ loop:
+ EnumRegKey $R7 SHCTX $R8 $R4
+ StrCmp $R7 "" end +1 ; if empty there are no more keys to enumerate
+ IntOp $R4 $R4 + 1 ; increment the loop's counter
+ ClearErrors
+ ReadRegStr $R5 SHCTX "$R8\$R7\bin" "PathToExe"
+ IfErrors loop
+
+ ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R5" $R5
+
+ IfFileExists "$R5" +1 loop
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${${_MOZFUNC_UN}GetParent} "$R5" $R6
+ StrCmp "$R6" "$R3" loop +1
+ StrCmp "$R6\${FileMainEXE}" "$R5" +1 loop
+ StrCpy $R9 "$R5"
+
+ end:
+ ClearErrors
+
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro GetSecondInstallPathCall _KEY _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call GetSecondInstallPath
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.GetSecondInstallPathCall _KEY _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call un.GetSecondInstallPath
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.GetSecondInstallPath
+ !ifndef un.GetSecondInstallPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro GetSecondInstallPath
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Finds an existing installation path for the application based on the
+ * application's executable name so we can default to using this path for the
+ * install. If there is zero or more than one installation of the application
+ * then we default to the default installation path. This uses SHCTX to
+ * determine the registry hive to read from so you must call SetShellVarContext
+ * first.
+ *
+ * @param _KEY
+ * The registry subkey (typically this will be Software\Mozilla\App Name).
+ * @return _RESULT
+ * false if a single install location for this app name isn't found,
+ * path to the install directory if a single install location is found.
+ *
+ * $R5 = counter for the loop's EnumRegKey
+ * $R6 = return value from EnumRegKey
+ * $R7 = return value from ReadRegStr
+ * $R8 = storage for _KEY
+ * $R9 = _KEY and _RESULT
+ */
+!macro GetSingleInstallPath
+
+ !ifndef ${_MOZFUNC_UN}GetSingleInstallPath
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}RemoveQuotesFromPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}GetSingleInstallPath "!insertmacro ${_MOZFUNC_UN}GetSingleInstallPathCall"
+
+ Function ${_MOZFUNC_UN}GetSingleInstallPath
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ StrCpy $R8 $R9
+ StrCpy $R9 "false"
+ StrCpy $R5 0 ; set the counter for the loop to 0
+
+ loop:
+ ClearErrors
+ EnumRegKey $R6 SHCTX $R8 $R5
+ IfErrors cleanup
+ StrCmp $R6 "" cleanup +1 ; if empty there are no more keys to enumerate
+ IntOp $R5 $R5 + 1 ; increment the loop's counter
+ ClearErrors
+ ReadRegStr $R7 SHCTX "$R8\$R6\Main" "PathToExe"
+ IfErrors loop
+ ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R7" $R7
+ GetFullPathName $R7 "$R7"
+ IfErrors loop
+
+ StrCmp "$R9" "false" +1 +3
+ StrCpy $R9 "$R7"
+ GoTo Loop
+
+ StrCpy $R9 "false"
+
+ cleanup:
+ StrCmp $R9 "false" end +1
+ ${${_MOZFUNC_UN}GetLongPath} "$R9" $R9
+ ${${_MOZFUNC_UN}GetParent} "$R9" $R9
+
+ end:
+ ClearErrors
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro GetSingleInstallPathCall _KEY _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call GetSingleInstallPath
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.GetSingleInstallPathCall _KEY _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call un.GetSingleInstallPath
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.GetSingleInstallPath
+ !ifndef un.GetSingleInstallPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro GetSingleInstallPath
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for working with the file system
+
+/**
+ * Attempts to delete a file if it exists. This will fail if the file is in use.
+ *
+ * @param _FILE
+ * The path to the file that is to be deleted.
+ */
+!macro DeleteFile _FILE
+ ${If} ${FileExists} "${_FILE}"
+ Delete "${_FILE}"
+ ${EndIf}
+!macroend
+!define DeleteFile "!insertmacro DeleteFile"
+
+/**
+ * Removes a directory if it exists and is empty.
+ *
+ * @param _DIR
+ * The path to the directory that is to be removed.
+ */
+!macro RemoveDir _DIR
+ ${If} ${FileExists} "${_DIR}"
+ RmDir "${_DIR}"
+ ${EndIf}
+!macroend
+!define RemoveDir "!insertmacro RemoveDir"
+
+/**
+ * Checks whether it is possible to create and delete a directory and a file in
+ * the install directory. Creation and deletion of files and directories are
+ * checked since a user may have rights for one and not the other. If creation
+ * and deletion of a file and a directory are successful this macro will return
+ * true... if not, this it return false.
+ *
+ * @return _RESULT
+ * true if files and directories can be created and deleted in the
+ * install directory otherwise false.
+ *
+ * $R8 = temporary filename in the installation directory returned from
+ * GetTempFileName.
+ * $R9 = _RESULT
+ */
+!macro CanWriteToInstallDir
+
+ !ifndef ${_MOZFUNC_UN}CanWriteToInstallDir
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CanWriteToInstallDir "!insertmacro ${_MOZFUNC_UN}CanWriteToInstallDirCall"
+
+ Function ${_MOZFUNC_UN}CanWriteToInstallDir
+ Push $R9
+ Push $R8
+
+ StrCpy $R9 "true"
+
+ ; IfFileExists returns false for $INSTDIR when $INSTDIR is the root of a
+ ; UNC path so always try to create $INSTDIR
+ CreateDirectory "$INSTDIR\"
+ GetTempFileName $R8 "$INSTDIR\"
+
+ ${Unless} ${FileExists} $R8 ; Can files be created?
+ StrCpy $R9 "false"
+ Goto done
+ ${EndUnless}
+
+ Delete $R8
+ ${If} ${FileExists} $R8 ; Can files be deleted?
+ StrCpy $R9 "false"
+ Goto done
+ ${EndIf}
+
+ CreateDirectory $R8
+ ${Unless} ${FileExists} $R8 ; Can directories be created?
+ StrCpy $R9 "false"
+ Goto done
+ ${EndUnless}
+
+ RmDir $R8
+ ${If} ${FileExists} $R8 ; Can directories be deleted?
+ StrCpy $R9 "false"
+ Goto done
+ ${EndIf}
+
+ done:
+
+ RmDir "$INSTDIR\" ; Only remove $INSTDIR if it is empty
+ ClearErrors
+
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CanWriteToInstallDirCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call CanWriteToInstallDir
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CanWriteToInstallDirCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.CanWriteToInstallDir
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CanWriteToInstallDir
+ !ifndef un.CanWriteToInstallDir
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CanWriteToInstallDir
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Checks whether there is sufficient free space available for the installation
+ * directory using GetDiskFreeSpaceExW which respects disk quotas. This macro
+ * will calculate the size of all sections that are selected, compare that with
+ * the free space available, and if there is sufficient free space it will
+ * return true... if not, it will return false.
+ *
+ * @return _RESULT
+ * "true" if there is sufficient free space otherwise "false".
+ *
+ * $R5 = return value from SectionGetSize
+ * $R6 = return value from SectionGetFlags
+ * return value from an 'and' comparison of SectionGetFlags (1=selected)
+ * return value for lpFreeBytesAvailable from GetDiskFreeSpaceExW
+ * return value for System::Int64Op $R6 / 1024
+ * return value for System::Int64Op $R6 > $R8
+ * $R7 = the counter for enumerating the sections
+ * the temporary file name for the directory created under $INSTDIR passed
+ * to GetDiskFreeSpaceExW.
+ * $R8 = sum in KB of all selected sections
+ * $R9 = _RESULT
+ */
+!macro CheckDiskSpace
+
+ !ifndef ${_MOZFUNC_UN}CheckDiskSpace
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CheckDiskSpace "!insertmacro ${_MOZFUNC_UN}CheckDiskSpaceCall"
+
+ Function ${_MOZFUNC_UN}CheckDiskSpace
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ ClearErrors
+
+ StrCpy $R9 "true" ; default return value
+ StrCpy $R8 "0" ; sum in KB of all selected sections
+ StrCpy $R7 "0" ; counter for enumerating sections
+
+ ; Enumerate the sections and sum up the sizes of the sections that are
+ ; selected.
+ SectionGetFlags $R7 $R6
+ IfErrors +7 +1
+ IntOp $R6 ${SF_SELECTED} & $R6
+ IntCmp $R6 0 +3 +1 +1
+ SectionGetSize $R7 $R5
+ IntOp $R8 $R8 + $R5
+ IntOp $R7 $R7 + 1
+ GoTo -7
+
+ ; The directory passed to GetDiskFreeSpaceExW must exist for the call to
+ ; succeed. Since the CanWriteToInstallDir macro is called prior to this
+ ; macro the call to CreateDirectory will always succeed.
+
+ ; IfFileExists returns false for $INSTDIR when $INSTDIR is the root of a
+ ; UNC path so always try to create $INSTDIR
+ CreateDirectory "$INSTDIR\"
+ GetTempFileName $R7 "$INSTDIR\"
+ Delete "$R7"
+ CreateDirectory "$R7"
+
+ System::Call 'kernel32::GetDiskFreeSpaceExW(w, *l, *l, *l) i(R7, .R6, ., .) .'
+
+ ; Convert to KB for comparison with $R8 which is in KB
+ System::Int64Op $R6 / 1024
+ Pop $R6
+
+ System::Int64Op $R6 > $R8
+ Pop $R6
+
+ IntCmp $R6 1 end +1 +1
+ StrCpy $R9 "false"
+
+ end:
+ RmDir "$R7"
+ RmDir "$INSTDIR\" ; Only remove $INSTDIR if it is empty
+
+ ClearErrors
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CheckDiskSpaceCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call CheckDiskSpace
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CheckDiskSpaceCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.CheckDiskSpace
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro un.CheckDiskSpace
+ !ifndef un.CheckDiskSpace
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CheckDiskSpace
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+* Returns the path found within a passed in string. The path is quoted or not
+* with the exception of an unquoted non 8dot3 path without arguments that is
+* also not a DefaultIcon path, is a 8dot3 path or not, has command line
+* arguments, or is a registry DefaultIcon path (e.g. <path to binary>,# where #
+* is the icon's resuorce id). The string does not need to be a valid path or
+* exist. It is up to the caller to pass in a string of one of the forms noted
+* above and to verify existence if necessary.
+*
+* Examples:
+* In: C:\PROGRA~1\MOZILL~1\FIREFOX.EXE -flag "%1"
+* In: C:\PROGRA~1\MOZILL~1\FIREFOX.EXE,0
+* In: C:\PROGRA~1\MOZILL~1\FIREFOX.EXE
+* In: "C:\PROGRA~1\MOZILL~1\FIREFOX.EXE"
+* In: "C:\PROGRA~1\MOZILL~1\FIREFOX.EXE" -flag "%1"
+* Out: C:\PROGRA~1\MOZILL~1\FIREFOX.EXE
+*
+* In: "C:\Program Files\Mozilla Firefox\firefox.exe" -flag "%1"
+* In: C:\Program Files\Mozilla Firefox\firefox.exe,0
+* In: "C:\Program Files\Mozilla Firefox\firefox.exe"
+* Out: C:\Program Files\Mozilla Firefox\firefox.exe
+*
+* @param _IN_PATH
+* The string containing the path.
+* @param _OUT_PATH
+* The register to store the path to.
+*
+* $R7 = counter for the outer loop's EnumRegKey
+* $R8 = return value from ReadRegStr
+* $R9 = _IN_PATH and _OUT_PATH
+*/
+!macro GetPathFromString
+
+ !ifndef ${_MOZFUNC_UN}GetPathFromString
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}GetPathFromString "!insertmacro ${_MOZFUNC_UN}GetPathFromStringCall"
+
+ Function ${_MOZFUNC_UN}GetPathFromString
+ Exch $R9
+ Push $R8
+ Push $R7
+
+ StrCpy $R7 0 ; Set the counter to 0.
+
+ ; Handle quoted paths with arguments.
+ StrCpy $R8 $R9 1 ; Copy the first char.
+ StrCmp $R8 '"' +2 +1 ; Is it a "?
+ StrCmp $R8 "'" +1 +9 ; Is it a '?
+ StrCpy $R9 $R9 "" 1 ; Remove the first char.
+ IntOp $R7 $R7 + 1 ; Increment the counter.
+ StrCpy $R8 $R9 1 $R7 ; Starting from the counter copy the next char.
+ StrCmp $R8 "" end +1 ; Are there no more chars?
+ StrCmp $R8 '"' +2 +1 ; Is it a " char?
+ StrCmp $R8 "'" +1 -4 ; Is it a ' char?
+ StrCpy $R9 $R9 $R7 ; Copy chars up to the counter.
+ GoTo end
+
+ ; Handle DefaultIcon paths. DefaultIcon paths are not quoted and end with
+ ; a , and a number.
+ IntOp $R7 $R7 - 1 ; Decrement the counter.
+ StrCpy $R8 $R9 1 $R7 ; Copy one char from the end minus the counter.
+ StrCmp $R8 '' +4 +1 ; Are there no more chars?
+ StrCmp $R8 ',' +1 -3 ; Is it a , char?
+ StrCpy $R9 $R9 $R7 ; Copy chars up to the end minus the counter.
+ GoTo end
+
+ ; Handle unquoted paths with arguments. An unquoted path with arguments
+ ; must be an 8dot3 path.
+ StrCpy $R7 -1 ; Set the counter to -1 so it will start at 0.
+ IntOp $R7 $R7 + 1 ; Increment the counter.
+ StrCpy $R8 $R9 1 $R7 ; Starting from the counter copy the next char.
+ StrCmp $R8 "" end +1 ; Are there no more chars?
+ StrCmp $R8 " " +1 -3 ; Is it a space char?
+ StrCpy $R9 $R9 $R7 ; Copy chars up to the counter.
+
+ end:
+ ClearErrors
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro GetPathFromStringCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call GetPathFromString
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.GetPathFromStringCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call un.GetPathFromString
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.GetPathFromString
+ !ifndef un.GetPathFromString
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro GetPathFromString
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Removes the quotes from each end of a string if present.
+ *
+ * @param _IN_PATH
+ * The string containing the path.
+ * @param _OUT_PATH
+ * The register to store the long path.
+ *
+ * $R7 = storage for single character comparison
+ * $R8 = storage for _IN_PATH
+ * $R9 = _IN_PATH and _OUT_PATH
+ */
+!macro RemoveQuotesFromPath
+
+ !ifndef ${_MOZFUNC_UN}RemoveQuotesFromPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RemoveQuotesFromPath "!insertmacro ${_MOZFUNC_UN}RemoveQuotesFromPathCall"
+
+ Function ${_MOZFUNC_UN}RemoveQuotesFromPath
+ Exch $R9
+ Push $R8
+ Push $R7
+
+ StrCpy $R7 "$R9" 1
+ StrCmp $R7 "$\"" +1 +2
+ StrCpy $R9 "$R9" "" 1
+
+ StrCpy $R7 "$R9" "" -1
+ StrCmp $R7 "$\"" +1 +2
+ StrCpy $R9 "$R9" -1
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RemoveQuotesFromPathCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call RemoveQuotesFromPath
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.RemoveQuotesFromPathCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call un.RemoveQuotesFromPath
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.RemoveQuotesFromPath
+ !ifndef un.RemoveQuotesFromPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RemoveQuotesFromPath
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Returns the long path for an existing file or directory. GetLongPathNameW
+ * may not be available on Win95 if Microsoft Layer for Unicode is not
+ * installed and GetFullPathName only returns a long path for the last file or
+ * directory that doesn't end with a \ in the path that it is passed. If the
+ * path does not exist on the file system this will return an empty string. To
+ * provide a consistent result trailing back-slashes are always removed.
+ *
+ * Note: 1024 used by GetLongPathNameW is the maximum NSIS string length.
+ *
+ * @param _IN_PATH
+ * The string containing the path.
+ * @param _OUT_PATH
+ * The register to store the long path.
+ *
+ * $R4 = counter value when the previous \ was found
+ * $R5 = directory or file name found during loop
+ * $R6 = return value from GetLongPathNameW and loop counter
+ * $R7 = long path from GetLongPathNameW and single char from path for comparison
+ * $R8 = storage for _IN_PATH
+ * $R9 = _IN_PATH _OUT_PATH
+ */
+!macro GetLongPath
+
+ !ifndef ${_MOZFUNC_UN}GetLongPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}GetLongPath "!insertmacro ${_MOZFUNC_UN}GetLongPathCall"
+
+ Function ${_MOZFUNC_UN}GetLongPath
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+
+ ClearErrors
+
+ GetFullPathName $R8 "$R9"
+ IfErrors end_GetLongPath +1 ; If the path doesn't exist return an empty string.
+
+ System::Call 'kernel32::GetLongPathNameW(w R8, w .R7, i 1024)i .R6'
+ StrCmp "$R7" "" +4 +1 ; Empty string when GetLongPathNameW is not present.
+ StrCmp $R6 0 +3 +1 ; Should never equal 0 since the path exists.
+ StrCpy $R9 "$R7"
+ GoTo end_GetLongPath
+
+ ; Do it the hard way.
+ StrCpy $R4 0 ; Stores the position in the string of the last \ found.
+ StrCpy $R6 -1 ; Set the counter to -1 so it will start at 0.
+
+ loop_GetLongPath:
+ IntOp $R6 $R6 + 1 ; Increment the counter.
+ StrCpy $R7 $R8 1 $R6 ; Starting from the counter copy the next char.
+ StrCmp $R7 "" +2 +1 ; Are there no more chars?
+ StrCmp $R7 "\" +1 -3 ; Is it a \?
+
+ ; Copy chars starting from the previously found \ to the counter.
+ StrCpy $R5 $R8 $R6 $R4
+
+ ; If this is the first \ found we want to swap R9 with R5 so a \ will
+ ; be appended to the drive letter and colon (e.g. C: will become C:\).
+ StrCmp $R4 0 +1 +3
+ StrCpy $R9 $R5
+ StrCpy $R5 ""
+
+ GetFullPathName $R9 "$R9\$R5"
+
+ StrCmp $R7 "" end_GetLongPath +1 ; Are there no more chars?
+
+ ; Store the counter for the current \ and prefix it for StrCpy operations.
+ StrCpy $R4 "+$R6"
+ IntOp $R6 $R6 + 1 ; Increment the counter so we skip over the \.
+ StrCpy $R8 $R8 "" $R6 ; Copy chars starting from the counter to the end.
+ StrCpy $R6 -1 ; Reset the counter to -1 so it will start over at 0.
+ GoTo loop_GetLongPath
+
+ end_GetLongPath:
+ ; If there is a trailing slash remove it
+ StrCmp $R9 "" +4 +1
+ StrCpy $R8 "$R9" "" -1
+ StrCmp $R8 "\" +1 +2
+ StrCpy $R9 "$R9" -1
+
+ ClearErrors
+
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro GetLongPathCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call GetLongPath
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.GetLongPathCall _IN_PATH _OUT_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_IN_PATH}"
+ Call un.GetLongPath
+ Pop ${_OUT_PATH}
+ !verbose pop
+!macroend
+
+!macro un.GetLongPath
+ !ifndef un.GetLongPath
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro GetLongPath
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for cleaning up the registry and file system
+
+/**
+ * Removes registry keys that reference this install location and for paths that
+ * no longer exist. This uses SHCTX to determine the registry hive so you must
+ * call SetShellVarContext first.
+ *
+ * @param _KEY
+ * The registry subkey (typically this will be Software\Mozilla).
+ *
+ * XXXrstrong - there is the potential for Key: Software/Mozilla/AppName,
+ * ValueName: CurrentVersion, ValueData: AppVersion to reference a key that is
+ * no longer available due to this cleanup. This should be no worse than prior
+ * to this reg cleanup since the referenced key would be for an app that is no
+ * longer installed on the system.
+ *
+ * $R0 = on x64 systems set to 'false' at the beginning of the macro when
+ * enumerating the x86 registry view and set to 'true' when enumerating
+ * the x64 registry view.
+ * $R1 = stores the long path to $INSTDIR
+ * $R2 = return value from the stack from the GetParent and GetLongPath macros
+ * $R3 = return value from the outer loop's EnumRegKey
+ * $R4 = return value from the inner loop's EnumRegKey
+ * $R5 = return value from ReadRegStr
+ * $R6 = counter for the outer loop's EnumRegKey
+ * $R7 = counter for the inner loop's EnumRegKey
+ * $R8 = return value from the stack from the RemoveQuotesFromPath macro
+ * $R9 = _KEY
+ */
+!macro RegCleanMain
+
+ !ifndef ${_MOZFUNC_UN}RegCleanMain
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}RemoveQuotesFromPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RegCleanMain "!insertmacro ${_MOZFUNC_UN}RegCleanMainCall"
+
+ Function ${_MOZFUNC_UN}RegCleanMain
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R1
+ StrCpy $R6 0 ; set the counter for the outer loop to 0
+
+ ${If} ${RunningX64}
+ StrCpy $R0 "false"
+ ; Set the registry to the 32 bit registry for 64 bit installations or to
+ ; the 64 bit registry for 32 bit installations at the beginning so it can
+ ; easily be set back to the correct registry view when finished.
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 32
+ !else
+ SetRegView 64
+ !endif
+ ${EndIf}
+
+ outerloop:
+ EnumRegKey $R3 SHCTX $R9 $R6
+ StrCmp $R3 "" end +1 ; if empty there are no more keys to enumerate
+ IntOp $R6 $R6 + 1 ; increment the outer loop's counter
+ ClearErrors
+ ReadRegStr $R5 SHCTX "$R9\$R3\bin" "PathToExe"
+ IfErrors 0 outercontinue
+ StrCpy $R7 0 ; set the counter for the inner loop to 0
+
+ innerloop:
+ EnumRegKey $R4 SHCTX "$R9\$R3" $R7
+ StrCmp $R4 "" outerloop +1 ; if empty there are no more keys to enumerate
+ IntOp $R7 $R7 + 1 ; increment the inner loop's counter
+ ClearErrors
+ ReadRegStr $R5 SHCTX "$R9\$R3\$R4\Main" "PathToExe"
+ IfErrors innerloop
+
+ ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R5" $R8
+ ${${_MOZFUNC_UN}GetParent} "$R8" $R2
+ ${${_MOZFUNC_UN}GetLongPath} "$R2" $R2
+ IfFileExists "$R2" +1 innerloop
+ StrCmp "$R2" "$R1" +1 innerloop
+
+ ClearErrors
+ DeleteRegKey SHCTX "$R9\$R3\$R4"
+ IfErrors innerloop
+ IntOp $R7 $R7 - 1 ; decrement the inner loop's counter when the key is deleted successfully.
+ ClearErrors
+ DeleteRegKey /ifempty SHCTX "$R9\$R3"
+ IfErrors innerloop outerdecrement
+
+ outercontinue:
+ ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R5" $R8
+ ${${_MOZFUNC_UN}GetParent} "$R8" $R2
+ ${${_MOZFUNC_UN}GetLongPath} "$R2" $R2
+ IfFileExists "$R2" +1 outerloop
+ StrCmp "$R2" "$R1" +1 outerloop
+
+ ClearErrors
+ DeleteRegKey SHCTX "$R9\$R3"
+ IfErrors outerloop
+
+ outerdecrement:
+ IntOp $R6 $R6 - 1 ; decrement the outer loop's counter when the key is deleted successfully.
+ GoTo outerloop
+
+ end:
+ ${If} ${RunningX64}
+ ${AndIf} "$R0" == "false"
+ ; Set the registry to the correct view.
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 64
+ !else
+ SetRegView 32
+ !endif
+
+ StrCpy $R6 0 ; set the counter for the outer loop to 0
+ StrCpy $R0 "true"
+ GoTo outerloop
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RegCleanMainCall _KEY
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call RegCleanMain
+ !verbose pop
+!macroend
+
+!macro un.RegCleanMainCall _KEY
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_KEY}"
+ Call un.RegCleanMain
+ !verbose pop
+!macroend
+
+!macro un.RegCleanMain
+ !ifndef un.RegCleanMain
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RegCleanMain
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Removes all registry keys from \Software\Windows\CurrentVersion\Uninstall
+ * that reference this install location in both the 32 bit and 64 bit registry
+ * view. This macro uses SHCTX to determine the registry hive so you must call
+ * SetShellVarContext first.
+ *
+ * $R3 = on x64 systems set to 'false' at the beginning of the macro when
+ * enumerating the x86 registry view and set to 'true' when enumerating
+ * the x64 registry view.
+ * $R4 = stores the long path to $INSTDIR
+ * $R5 = return value from ReadRegStr
+ * $R6 = string for the base reg key (e.g. Software\Microsoft\Windows\CurrentVersion\Uninstall)
+ * $R7 = return value from EnumRegKey
+ * $R8 = counter for EnumRegKey
+ * $R9 = return value from the stack from the RemoveQuotesFromPath and GetLongPath macros
+ */
+!macro RegCleanUninstall
+
+ !ifndef ${_MOZFUNC_UN}RegCleanUninstall
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}RemoveQuotesFromPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RegCleanUninstall "!insertmacro ${_MOZFUNC_UN}RegCleanUninstallCall"
+
+ Function ${_MOZFUNC_UN}RegCleanUninstall
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R4
+ StrCpy $R6 "Software\Microsoft\Windows\CurrentVersion\Uninstall"
+ StrCpy $R7 ""
+ StrCpy $R8 0
+
+ ${If} ${RunningX64}
+ StrCpy $R3 "false"
+ ; Set the registry to the 32 bit registry for 64 bit installations or to
+ ; the 64 bit registry for 32 bit installations at the beginning so it can
+ ; easily be set back to the correct registry view when finished.
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 32
+ !else
+ SetRegView 64
+ !endif
+ ${EndIf}
+
+ loop:
+ EnumRegKey $R7 SHCTX $R6 $R8
+ StrCmp $R7 "" end +1
+ IntOp $R8 $R8 + 1 ; Increment the counter
+ ClearErrors
+ ReadRegStr $R5 SHCTX "$R6\$R7" "InstallLocation"
+ IfErrors loop
+ ${${_MOZFUNC_UN}RemoveQuotesFromPath} "$R5" $R9
+
+ ; Detect when the path is just a drive letter without a trailing
+ ; backslash (e.g., "C:"), and add a backslash. If we don't, the Win32
+ ; calls in GetLongPath will interpret that syntax as a shorthand
+ ; for the working directory, because that's the DOS 2.0 convention,
+ ; and will return the path to that directory instead of just the drive.
+ ; Back here, we would then successfully match that with our $INSTDIR,
+ ; and end up deleting a registry key that isn't really ours.
+ StrLen $R5 "$R9"
+ ${If} $R5 == 2
+ StrCpy $R5 "$R9" 1 1
+ ${If} "$R5" == ":"
+ StrCpy $R9 "$R9\"
+ ${EndIf}
+ ${EndIf}
+
+ ${${_MOZFUNC_UN}GetLongPath} "$R9" $R9
+ StrCmp "$R9" "$R4" +1 loop
+ ClearErrors
+ DeleteRegKey SHCTX "$R6\$R7"
+ IfErrors loop +1
+ IntOp $R8 $R8 - 1 ; Decrement the counter on successful deletion
+ GoTo loop
+
+ end:
+ ${If} ${RunningX64}
+ ${AndIf} "$R3" == "false"
+ ; Set the registry to the correct view.
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 64
+ !else
+ SetRegView 32
+ !endif
+
+ StrCpy $R7 ""
+ StrCpy $R8 0
+ StrCpy $R3 "true"
+ GoTo loop
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RegCleanUninstallCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call RegCleanUninstall
+ !verbose pop
+!macroend
+
+!macro un.RegCleanUninstallCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.RegCleanUninstall
+ !verbose pop
+!macroend
+
+!macro un.RegCleanUninstall
+ !ifndef un.RegCleanUninstall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RegCleanUninstall
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Removes an application specific handler registry key under Software\Classes
+ * for both HKCU and HKLM when its open command refers to this install
+ * location or the install location doesn't exist.
+ *
+ * @param _HANDLER_NAME
+ * The registry name for the handler.
+ *
+ * $R7 = stores the long path to the $INSTDIR
+ * $R8 = stores the path to the open command's parent directory
+ * $R9 = _HANDLER_NAME
+ */
+!macro RegCleanAppHandler
+
+ !ifndef ${_MOZFUNC_UN}RegCleanAppHandler
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}GetPathFromString
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RegCleanAppHandler "!insertmacro ${_MOZFUNC_UN}RegCleanAppHandlerCall"
+
+ Function ${_MOZFUNC_UN}RegCleanAppHandler
+ Exch $R9
+ Push $R8
+ Push $R7
+
+ ClearErrors
+ ReadRegStr $R8 HKCU "Software\Classes\$R9\shell\open\command" ""
+ IfErrors next +1
+ ${${_MOZFUNC_UN}GetPathFromString} "$R8" $R8
+ ${${_MOZFUNC_UN}GetParent} "$R8" $R8
+ IfFileExists "$R8" +3 +1
+ DeleteRegKey HKCU "Software\Classes\$R9"
+ GoTo next
+
+ ${${_MOZFUNC_UN}GetLongPath} "$R8" $R8
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R7
+ StrCmp "$R7" "$R8" +1 next
+ DeleteRegKey HKCU "Software\Classes\$R9"
+
+ next:
+ ReadRegStr $R8 HKLM "Software\Classes\$R9\shell\open\command" ""
+ IfErrors end
+ ${${_MOZFUNC_UN}GetPathFromString} "$R8" $R8
+ ${${_MOZFUNC_UN}GetParent} "$R8" $R8
+ IfFileExists "$R8" +3 +1
+ DeleteRegKey HKLM "Software\Classes\$R9"
+ GoTo end
+
+ ${${_MOZFUNC_UN}GetLongPath} "$R8" $R8
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R7
+ StrCmp "$R7" "$R8" +1 end
+ DeleteRegKey HKLM "Software\Classes\$R9"
+
+ end:
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RegCleanAppHandlerCall _HANDLER_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_HANDLER_NAME}"
+ Call RegCleanAppHandler
+ !verbose pop
+!macroend
+
+!macro un.RegCleanAppHandlerCall _HANDLER_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_HANDLER_NAME}"
+ Call un.RegCleanAppHandler
+ !verbose pop
+!macroend
+
+!macro un.RegCleanAppHandler
+ !ifndef un.RegCleanAppHandler
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RegCleanAppHandler
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Cleans up the registry for a protocol handler when its open command
+ * refers to this install location. For HKCU the registry key is deleted
+ * and for HKLM the values set by the application are deleted.
+ *
+ * @param _HANDLER_NAME
+ * The registry name for the handler.
+ *
+ * $R7 = stores the long path to $INSTDIR
+ * $R8 = stores the the long path to the open command's parent directory
+ * $R9 = _HANDLER_NAME
+ */
+!macro un.RegCleanProtocolHandler
+
+ !ifndef un.RegCleanProtocolHandler
+ !insertmacro un.GetLongPath
+ !insertmacro un.GetParent
+ !insertmacro un.GetPathFromString
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define un.RegCleanProtocolHandler "!insertmacro un.RegCleanProtocolHandlerCall"
+
+ Function un.RegCleanProtocolHandler
+ Exch $R9
+ Push $R8
+ Push $R7
+
+ ReadRegStr $R8 HKCU "Software\Classes\$R9\shell\open\command" ""
+ ${un.GetLongPath} "$INSTDIR" $R7
+
+ StrCmp "$R8" "" next +1
+ ${un.GetPathFromString} "$R8" $R8
+ ${un.GetParent} "$R8" $R8
+ ${un.GetLongPath} "$R8" $R8
+ StrCmp "$R7" "$R8" +1 next
+ DeleteRegKey HKCU "Software\Classes\$R9"
+
+ next:
+ ReadRegStr $R8 HKLM "Software\Classes\$R9\shell\open\command" ""
+ StrCmp "$R8" "" end +1
+ ${un.GetLongPath} "$INSTDIR" $R7
+ ${un.GetPathFromString} "$R8" $R8
+ ${un.GetParent} "$R8" $R8
+ ${un.GetLongPath} "$R8" $R8
+ StrCmp "$R7" "$R8" +1 end
+ DeleteRegValue HKLM "Software\Classes\$R9\DefaultIcon" ""
+ DeleteRegValue HKLM "Software\Classes\$R9\shell\open" ""
+ DeleteRegValue HKLM "Software\Classes\$R9\shell\open\command" ""
+ DeleteRegValue HKLM "Software\Classes\$R9\shell\ddeexec" ""
+ DeleteRegValue HKLM "Software\Classes\$R9\shell\ddeexec\Application" ""
+ DeleteRegValue HKLM "Software\Classes\$R9\shell\ddeexec\Topic" ""
+
+ end:
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro un.RegCleanProtocolHandlerCall _HANDLER_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_HANDLER_NAME}"
+ Call un.RegCleanProtocolHandler
+ !verbose pop
+!macroend
+
+/**
+ * Cleans up the registry for a file handler when the passed in value equals
+ * the default value for the file handler. For HKCU the registry key is deleted
+ * and for HKLM the default value is deleted.
+ *
+ * @param _HANDLER_NAME
+ * The registry name for the handler.
+ * @param _DEFAULT_VALUE
+ * The value to check for against the handler's default value.
+ *
+ * $R6 = stores the long path to $INSTDIR
+ * $R7 = _DEFAULT_VALUE
+ * $R9 = _HANDLER_NAME
+ */
+!macro RegCleanFileHandler
+
+ !ifndef ${_MOZFUNC_UN}RegCleanFileHandler
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}GetPathFromString
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RegCleanFileHandler "!insertmacro ${_MOZFUNC_UN}RegCleanFileHandlerCall"
+
+ Function ${_MOZFUNC_UN}RegCleanFileHandler
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Push $R7
+
+ DeleteRegValue HKCU "Software\Classes\$R9\OpenWithProgids" $R8
+ EnumRegValue $R7 HKCU "Software\Classes\$R9\OpenWithProgids" 0
+ StrCmp "$R7" "" +1 +2
+ DeleteRegKey HKCU "Software\Classes\$R9\OpenWithProgids"
+ ReadRegStr $R7 HKCU "Software\Classes\$R9" ""
+ StrCmp "$R7" "$R8" +1 +2
+ DeleteRegKey HKCU "Software\Classes\$R9"
+
+ DeleteRegValue HKLM "Software\Classes\$R9\OpenWithProgids" $R8
+ EnumRegValue $R7 HKLM "Software\Classes\$R9\OpenWithProgids" 0
+ StrCmp "$R7" "" +1 +2
+ DeleteRegKey HKLM "Software\Classes\$R9\OpenWithProgids"
+ ReadRegStr $R7 HKLM "Software\Classes\$R9" ""
+ StrCmp "$R7" "$R8" +1 +2
+ DeleteRegValue HKLM "Software\Classes\$R9" ""
+
+ ClearErrors
+
+ Pop $R7
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RegCleanFileHandlerCall _HANDLER_NAME _DEFAULT_VALUE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_DEFAULT_VALUE}"
+ Push "${_HANDLER_NAME}"
+ Call RegCleanFileHandler
+ !verbose pop
+!macroend
+
+!macro un.RegCleanFileHandlerCall _HANDLER_NAME _DEFAULT_VALUE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_DEFAULT_VALUE}"
+ Push "${_HANDLER_NAME}"
+ Call un.RegCleanFileHandler
+ !verbose pop
+!macroend
+
+!macro un.RegCleanFileHandler
+ !ifndef un.RegCleanFileHandler
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RegCleanFileHandler
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Checks if a handler's open command points to this installation directory.
+ * Uses SHCTX to determine the registry hive (e.g. HKLM or HKCU) to check.
+ *
+ * @param _HANDLER_NAME
+ * The registry name for the handler.
+ * @param _RESULT
+ * true if it is the handler's open command points to this
+ * installation directory and false if it does not.
+ *
+ * $R7 = stores the value of the open command and the path macros return values
+ * $R8 = stores the handler's registry key name
+ * $R9 = _DEFAULT_VALUE and _RESULT
+ */
+!macro IsHandlerForInstallDir
+
+ !ifndef ${_MOZFUNC_UN}IsHandlerForInstallDir
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}GetPathFromString
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}IsHandlerForInstallDir "!insertmacro ${_MOZFUNC_UN}IsHandlerForInstallDirCall"
+
+ Function ${_MOZFUNC_UN}IsHandlerForInstallDir
+ Exch $R9
+ Push $R8
+ Push $R7
+
+ StrCpy $R8 "$R9"
+ StrCpy $R9 "false"
+ ReadRegStr $R7 SHCTX "Software\Classes\$R8\shell\open\command" ""
+
+ ${If} $R7 != ""
+ ${GetPathFromString} "$R7" $R7
+ ${GetParent} "$R7" $R7
+ ${GetLongPath} "$R7" $R7
+ ${If} $R7 == $INSTDIR
+ StrCpy $R9 "true"
+ ${EndIf}
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro IsHandlerForInstallDirCall _HANDLER_NAME _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_HANDLER_NAME}"
+ Call IsHandlerForInstallDir
+ Pop "${_RESULT}"
+ !verbose pop
+!macroend
+
+!macro un.IsHandlerForInstallDirCall _HANDLER_NAME _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_HANDLER_NAME}"
+ Call un.IsHandlerForInstallDir
+ Pop "${_RESULT}"
+ !verbose pop
+!macroend
+
+!macro un.IsHandlerForInstallDir
+ !ifndef un.IsHandlerForInstallDir
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro IsHandlerForInstallDir
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Removes the application's VirtualStore directory if present when the
+ * installation directory is a sub-directory of the program files directory.
+ *
+ * $R4 = $PROGRAMFILES/$PROGRAMFILES64 for CleanVirtualStore_Internal
+ * $R5 = various path values.
+ * $R6 = length of the long path to $PROGRAMFILES32 or $PROGRAMFILES64
+ * $R7 = long path to $PROGRAMFILES32 or $PROGRAMFILES64
+ * $R8 = length of the long path to $INSTDIR
+ * $R9 = long path to $INSTDIR
+ */
+!macro CleanVirtualStore
+
+ !ifndef ${_MOZFUNC_UN}CleanVirtualStore
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CleanVirtualStore "!insertmacro ${_MOZFUNC_UN}CleanVirtualStoreCall"
+
+ Function ${_MOZFUNC_UN}CleanVirtualStore
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R9
+ ${If} "$R9" != ""
+ StrLen $R8 "$R9"
+
+ StrCpy $R4 $PROGRAMFILES32
+ Call ${_MOZFUNC_UN}CleanVirtualStore_Internal
+
+ ${If} ${RunningX64}
+ StrCpy $R4 $PROGRAMFILES64
+ Call ${_MOZFUNC_UN}CleanVirtualStore_Internal
+ ${EndIf}
+
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Pop $R9
+ FunctionEnd
+
+ Function ${_MOZFUNC_UN}CleanVirtualStore_Internal
+ ${${_MOZFUNC_UN}GetLongPath} "" $R7
+ ${If} "$R7" != ""
+ StrLen $R6 "$R7"
+ ${If} $R8 < $R6
+ ; Copy from the start of $INSTDIR the length of $PROGRAMFILES64
+ StrCpy $R5 "$R9" $R6
+ ${If} "$R5" == "$R7"
+ ; Remove the drive letter and colon from the $INSTDIR long path
+ StrCpy $R5 "$R9" "" 2
+ StrCpy $R5 "$LOCALAPPDATA\VirtualStore$R5"
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$R5" != ""
+ ${AndIf} ${FileExists} "$R5"
+ RmDir /r "$R5"
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CleanVirtualStoreCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call CleanVirtualStore
+ !verbose pop
+!macroend
+
+!macro un.CleanVirtualStoreCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.CleanVirtualStore
+ !verbose pop
+!macroend
+
+!macro un.CleanVirtualStore
+ !ifndef un.CleanVirtualStore
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CleanVirtualStore
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * If present removes the updates directory located in the profile's local
+ * directory for this installation.
+ * This macro is obsolete and should no longer be used. Please see
+ * CleanUpdateDirectories.
+ *
+ * @param _REL_PROFILE_PATH
+ * The relative path to the profile directory from $LOCALAPPDATA.
+ *
+ * $R4 = various path values.
+ * $R5 = length of the long path to $PROGRAMFILES
+ * $R6 = length of the long path to $INSTDIR
+ * $R7 = long path to $PROGRAMFILES
+ * $R8 = long path to $INSTDIR
+ * $R9 = _REL_PROFILE_PATH
+ */
+!macro CleanUpdatesDir
+
+ !ifndef ${_MOZFUNC_UN}CleanUpdatesDir
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CleanUpdatesDir "!insertmacro ${_MOZFUNC_UN}CleanUpdatesDirCall"
+
+ Function ${_MOZFUNC_UN}CleanUpdatesDir
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+
+ StrCmp $R9 "" end +1 ; The relative path to the app's profiles is required
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R8
+ StrCmp $R8 "" end +1
+ ${${_MOZFUNC_UN}GetLongPath} "$PROGRAMFILES" $R7
+ StrCmp $R7 "" end +1
+
+ StrLen $R6 "$R8"
+ StrLen $R5 "$R7"
+ ; Only continue If the length of $INSTDIR is greater than the length of
+ ; $PROGRAMFILES
+ IntCmp $R6 $R5 end end +1
+
+ ; Copy from the start of $INSTDIR the length of $PROGRAMFILES
+ StrCpy $R4 "$R8" $R5
+ StrCmp "$R4" "$R7" +1 end ; Check if $INSTDIR is under $PROGRAMFILES
+
+ ; Copy the relative path to $INSTDIR from $PROGRAMFILES
+ StrCpy $R4 "$R8" "" $R5
+
+ ; Concatenate the path to $LOCALAPPDATA the relative profile path and the
+ ; relative path to $INSTDIR from $PROGRAMFILES
+ StrCpy $R4 "$LOCALAPPDATA\$R9$R4"
+ ${${_MOZFUNC_UN}GetLongPath} "$R4" $R4
+ StrCmp $R4 "" end +1
+
+ IfFileExists "$R4\updates" +1 end
+ RmDir /r "$R4"
+
+ end:
+ ClearErrors
+
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CleanUpdatesDirCall _REL_PROFILE_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REL_PROFILE_PATH}"
+ Call CleanUpdatesDir
+ !verbose pop
+!macroend
+
+!macro un.CleanUpdatesDirCall _REL_PROFILE_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REL_PROFILE_PATH}"
+ Call un.CleanUpdatesDir
+ !verbose pop
+!macroend
+
+!macro un.CleanUpdatesDir
+ !ifndef un.CleanUpdatesDir
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CleanUpdatesDir
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * If present removes the updates directory located in the profile's local
+ * directory for this installation.
+ *
+ * @param _OLD_REL_PATH
+ * The relative path to the profile directory from $LOCALAPPDATA.
+ * Calculated for the old update directory not based on a hash.
+ * @param _NEW_REL_PATH
+ * The relative path to the profile directory from $LOCALAPPDATA.
+ * Calculated for the new update directory based on a hash.
+ *
+ * $R8 = _NEW_REL_PATH
+ * $R7 = _OLD_REL_PATH
+ * $R1 = taskBar ID hash located in registry at SOFTWARE\_OLD_REL_PATH\TaskBarIDs
+ * $R2 = various path values.
+ * $R3 = length of the long path to $PROGRAMFILES
+ * $R4 = length of the long path to $INSTDIR
+ * $R5 = long path to $PROGRAMFILES
+ * $R6 = long path to $INSTDIR
+ * $R0 = path to the new update directory built from _NEW_REL_PATH and
+ * the taskbar ID.
+ */
+!macro CleanUpdateDirectories
+
+ !ifndef ${_MOZFUNC_UN}CleanUpdateDirectories
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}CleanUpdateDirectories "!insertmacro ${_MOZFUNC_UN}CleanUpdateDirectoriesCall"
+
+ Function ${_MOZFUNC_UN}CleanUpdateDirectories
+ Exch $R8
+ Exch 1
+ Exch $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR" $R6
+ StrLen $R4 "$R6"
+
+ ${${_MOZFUNC_UN}GetLongPath} "$PROGRAMFILES" $R5
+ StrLen $R3 "$R5"
+
+ ${If} $R7 != "" ; _OLD_REL_PATH was passed
+ ${AndIf} $R6 != "" ; We have the install dir path
+ ${AndIf} $R5 != "" ; We the program files path
+ ${AndIf} $R4 > $R3 ; The length of $INSTDIR > the length of $PROGRAMFILES
+
+ ; Copy from the start of $INSTDIR the length of $PROGRAMFILES
+ StrCpy $R2 "$R6" $R3
+
+ ; Check if $INSTDIR is under $PROGRAMFILES
+ ${If} $R2 == $R5
+
+ ; Copy the relative path to $INSTDIR from $PROGRAMFILES
+ StrCpy $R2 "$R6" "" $R3
+
+ ; Concatenate the path $LOCALAPPDATA to the relative profile path and
+ ; the relative path to $INSTDIR from $PROGRAMFILES
+ StrCpy $R2 "$LOCALAPPDATA\$R7$R2"
+ ${${_MOZFUNC_UN}GetLongPath} "$R2" $R2
+
+ ${If} $R2 != ""
+ ; Backup the old update directory logs and delete the directory
+ ${If} ${FileExists} "$R2\updates\last-update.log"
+ Rename "$R2\updates\last-update.log" "$TEMP\moz-update-old-last-update.log"
+ ${EndIf}
+
+ ${If} ${FileExists} "$R2\updates\backup-update.log"
+ Rename "$R2\updates\backup-update.log" "$TEMP\moz-update-old-backup-update.log"
+ ${EndIf}
+
+ ${If} ${FileExists} "$R2\updates"
+ RmDir /r "$R2"
+ ${EndIf}
+ ${EndIf}
+
+ ; Get the taskbar ID hash for this installation path
+ ReadRegStr $R1 HKLM "SOFTWARE\$R7\TaskBarIDs" $R6
+ ${If} $R1 == ""
+ ReadRegStr $R1 HKCU "SOFTWARE\$R7\TaskBarIDs" $R6
+ ${EndIf}
+
+ ; If the taskbar ID hash exists then delete the new update directory
+ ; Backup its logs before deleting it.
+ ${If} $R1 != ""
+ StrCpy $R0 "$LOCALAPPDATA\$R8\$R1"
+
+ ${If} ${FileExists} "$R0\updates\last-update.log"
+ Rename "$R0\updates\last-update.log" "$TEMP\moz-update-new-last-update.log"
+ ${EndIf}
+
+ ${If} ${FileExists} "$R0\updates\backup-update.log"
+ Rename "$R0\updates\backup-update.log" "$TEMP\moz-update-new-backup-update.log"
+ ${EndIf}
+
+ ; Remove the old updates directory
+ ${If} ${FileExists} "$R0\updates"
+ RmDir /r "$R0"
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Exch $R7
+ Exch 1
+ Exch $R8
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CleanUpdateDirectoriesCall _OLD_REL_PATH _NEW_REL_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_OLD_REL_PATH}"
+ Push "${_NEW_REL_PATH}"
+ Call CleanUpdateDirectories
+ !verbose pop
+!macroend
+
+!macro un.CleanUpdateDirectoriesCall _OLD_REL_PATH _NEW_REL_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_OLD_REL_PATH}"
+ Push "${_NEW_REL_PATH}"
+ Call un.CleanUpdateDirectories
+ !verbose pop
+!macroend
+
+!macro un.CleanUpdateDirectories
+ !ifndef un.CleanUpdateDirectories
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro CleanUpdateDirectories
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Deletes all relative profiles specified in an application's profiles.ini and
+ * performs various other cleanup.
+ *
+ * @param _REL_PROFILE_PATH
+ * The relative path to the profile directory.
+ *
+ * $R6 = value of IsRelative read from profiles.ini
+ * $R7 = value of Path to profile read from profiles.ini
+ * $R8 = counter for reading profiles (e.g. Profile0, Profile1, etc.)
+ * $R9 = _REL_PROFILE_PATH
+ */
+!macro DeleteRelativeProfiles
+
+ !ifndef ${_MOZFUNC_UN}DeleteRelativeProfiles
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}WordReplace
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}DeleteRelativeProfiles "!insertmacro ${_MOZFUNC_UN}DeleteRelativeProfilesCall"
+
+ Function ${_MOZFUNC_UN}DeleteRelativeProfiles
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+
+ SetShellVarContext current
+ StrCpy $R8 -1
+
+ loop:
+ IntOp $R8 $R8 + 1 ; Increment the counter.
+ ReadINIStr $R7 "$APPDATA\$R9\profiles.ini" "Profile$R8" "Path"
+ IfErrors end +1
+
+ ; Only remove relative profiles
+ ReadINIStr $R6 "$APPDATA\$R9\profiles.ini" "Profile$R8" "IsRelative"
+ StrCmp "$R6" "1" +1 loop
+
+ ; Relative paths in profiles.ini use / as a separator
+ ${${_MOZFUNC_UN}WordReplace} "$R7" "/" "\" "+" $R7
+
+ IfFileExists "$LOCALAPPDATA\$R9\$R7" +1 +2
+ RmDir /r "$LOCALAPPDATA\$R9\$R7"
+ IfFileExists "$APPDATA\$R9\$R7" +1 +2
+ RmDir /r "$APPDATA\$R9\$R7"
+ GoTo loop
+
+ end:
+ ; Remove profiles directory under LOCALAPPDATA (e.g. cache, etc.) since
+ ; they are at times abandoned.
+ RmDir /r "$LOCALAPPDATA\$R9\Profiles"
+ RmDir /r "$APPDATA\$R9\Crash Reports"
+ Delete "$APPDATA\$R9\profiles.ini"
+ Delete "$APPDATA\$R9\console.log"
+ Delete "$APPDATA\$R9\pluginreg.dat"
+ RmDir "$APPDATA\$R9\Profiles"
+ RmDir "$APPDATA\$R9"
+
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro DeleteRelativeProfilesCall _REL_PROFILE_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REL_PROFILE_PATH}"
+ Call DeleteRelativeProfiles
+ !verbose pop
+!macroend
+
+!macro un.DeleteRelativeProfilesCall _REL_PROFILE_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REL_PROFILE_PATH}"
+ Call un.DeleteRelativeProfiles
+ !verbose pop
+!macroend
+
+!macro un.DeleteRelativeProfiles
+ !ifndef un.DeleteRelativeProfiles
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro DeleteRelativeProfiles
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Deletes shortcuts and Start Menu directories under Programs as specified by
+ * the shortcuts log ini file and on Windows 7 unpins TaskBar and Start Menu
+ * shortcuts. The shortcuts will not be deleted if the shortcut target isn't for
+ * this install location which is determined by the shortcut having a target of
+ * $INSTDIR\${FileMainEXE}. The context (All Users or Current User) of the
+ * $DESKTOP and $SMPROGRAMS constants depends on the
+ * SetShellVarContext setting and must be set by the caller of this macro. There
+ * is no All Users context for $QUICKLAUNCH but this will not cause a problem
+ * since the macro will just continue past the $QUICKLAUNCH shortcut deletion
+ * section on subsequent calls.
+ *
+ * The ini file sections must have the following format (the order of the
+ * sections in the ini file is not important):
+ * [SMPROGRAMS]
+ * ; RelativePath is the directory relative from the Start Menu
+ * ; Programs directory.
+ * RelativePath=Mozilla App
+ * ; Shortcut1 is the first shortcut, Shortcut2 is the second shortcut, and so
+ * ; on. There must not be a break in the sequence of the numbers.
+ * Shortcut1=Mozilla App.lnk
+ * Shortcut2=Mozilla App (Safe Mode).lnk
+ * [DESKTOP]
+ * ; Shortcut1 is the first shortcut, Shortcut2 is the second shortcut, and so
+ * ; on. There must not be a break in the sequence of the numbers.
+ * Shortcut1=Mozilla App.lnk
+ * Shortcut2=Mozilla App (Safe Mode).lnk
+ * [QUICKLAUNCH]
+ * ; Shortcut1 is the first shortcut, Shortcut2 is the second shortcut, and so
+ * ; on. There must not be a break in the sequence of the numbers for the
+ * ; suffix.
+ * Shortcut1=Mozilla App.lnk
+ * Shortcut2=Mozilla App (Safe Mode).lnk
+ * [STARTMENU]
+ * ; Shortcut1 is the first shortcut, Shortcut2 is the second shortcut, and so
+ * ; on. There must not be a break in the sequence of the numbers for the
+ * ; suffix.
+ * Shortcut1=Mozilla App.lnk
+ * Shortcut2=Mozilla App (Safe Mode).lnk
+ *
+ * $R4 = counter for appending to Shortcut for enumerating the ini file entries
+ * $R5 = return value from ShellLink::GetShortCutTarget and
+ * ApplicationID::UninstallPinnedItem
+ * $R6 = find handle and the long path to the Start Menu Programs directory
+ * (e.g. $SMPROGRAMS)
+ * $R7 = path to the $QUICKLAUNCH\User Pinned directory and the return value
+ * from ReadINIStr for the relative path to the applications directory
+ * under the Start Menu Programs directory and the long path to this
+ * directory
+ * $R8 = return filename from FindFirst / FindNext and the return value from
+ * ReadINIStr for enumerating shortcuts
+ * $R9 = long path to the shortcuts log ini file
+ */
+!macro DeleteShortcuts
+
+ !ifndef ${_MOZFUNC_UN}DeleteShortcuts
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}DeleteShortcuts "!insertmacro ${_MOZFUNC_UN}DeleteShortcutsCall"
+
+ Function ${_MOZFUNC_UN}DeleteShortcuts
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+
+ ${If} ${AtLeastWin7}
+ ; Since shortcuts that are pinned can later be removed without removing
+ ; the pinned shortcut unpin the pinned shortcuts for the application's
+ ; main exe using the pinned shortcuts themselves.
+ StrCpy $R7 "$QUICKLAUNCH\User Pinned"
+
+ ${If} ${FileExists} "$R7\TaskBar"
+ ; Delete TaskBar pinned shortcuts for the application's main exe
+ FindFirst $R6 $R8 "$R7\TaskBar\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$R7\TaskBar\$R8"
+ ShellLink::GetShortCutTarget "$R7\TaskBar\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$R5" == "$INSTDIR\${FileMainEXE}"
+ ApplicationID::UninstallPinnedItem "$R7\TaskBar\$R8"
+ Pop $R5
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R6 $R8
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R6
+ ${EndIf}
+
+ ${If} ${FileExists} "$R7\StartMenu"
+ ; Delete Start Menu pinned shortcuts for the application's main exe
+ FindFirst $R6 $R8 "$R7\StartMenu\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$R7\StartMenu\$R8"
+ ShellLink::GetShortCutTarget "$R7\StartMenu\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$R5" == "$INSTDIR\${FileMainEXE}"
+ ApplicationID::UninstallPinnedItem "$R7\StartMenu\$R8"
+ Pop $R5
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R6 $R8
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R6
+ ${EndIf}
+ ${EndIf}
+
+ ; Don't call ApplicationID::UninstallPinnedItem since pinned items for
+ ; this application were removed above and removing them below will remove
+ ; the association of side by side installations.
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR\uninstall\${SHORTCUTS_LOG}" $R9
+ ${If} ${FileExists} "$R9"
+ ; Delete Start Menu shortcuts for this application
+ StrCpy $R4 -1
+ ${Do}
+ IntOp $R4 $R4 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R8 "$R9" "STARTMENU" "Shortcut$R4"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$SMPROGRAMS\$R8"
+ ShellLink::GetShortCutTarget "$SMPROGRAMS\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$INSTDIR\${FileMainEXE}" == "$R5"
+ Delete "$SMPROGRAMS\$R8"
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ; Delete Quick Launch shortcuts for this application
+ StrCpy $R4 -1
+ ${Do}
+ IntOp $R4 $R4 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R8 "$R9" "QUICKLAUNCH" "Shortcut$R4"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$QUICKLAUNCH\$R8"
+ ShellLink::GetShortCutTarget "$QUICKLAUNCH\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$INSTDIR\${FileMainEXE}" == "$R5"
+ Delete "$QUICKLAUNCH\$R8"
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ; Delete Desktop shortcuts for this application
+ StrCpy $R4 -1
+ ${Do}
+ IntOp $R4 $R4 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R8 "$R9" "DESKTOP" "Shortcut$R4"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$DESKTOP\$R8"
+ ShellLink::GetShortCutTarget "$DESKTOP\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$INSTDIR\${FileMainEXE}" == "$R5"
+ Delete "$DESKTOP\$R8"
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ${${_MOZFUNC_UN}GetLongPath} "$SMPROGRAMS" $R6
+
+ ; Delete Start Menu Programs shortcuts for this application
+ ClearErrors
+ ReadINIStr $R7 "$R9" "SMPROGRAMS" "RelativePathToDir"
+ ${${_MOZFUNC_UN}GetLongPath} "$R6\$R7" $R7
+ ${Unless} "$R7" == ""
+ StrCpy $R4 -1
+ ${Do}
+ IntOp $R4 $R4 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R8 "$R9" "SMPROGRAMS" "Shortcut$R4"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$R7\$R8"
+ ShellLink::GetShortCutTarget "$R7\$R8"
+ Pop $R5
+ ${${_MOZFUNC_UN}GetLongPath} "$R5" $R5
+ ${If} "$INSTDIR\${FileMainEXE}" == "$R5"
+ Delete "$R7\$R8"
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ; Delete Start Menu Programs directories for this application
+ ${Do}
+ ClearErrors
+ ${If} "$R6" == "$R7"
+ ${ExitDo}
+ ${EndIf}
+ RmDir "$R7"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${${_MOZFUNC_UN}GetParent} "$R7" $R7
+ ${Loop}
+ ${EndUnless}
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro DeleteShortcutsCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call DeleteShortcuts
+ !verbose pop
+!macroend
+
+!macro un.DeleteShortcutsCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.DeleteShortcuts
+ !verbose pop
+!macroend
+
+!macro un.DeleteShortcuts
+ !ifndef un.DeleteShortcuts
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro DeleteShortcuts
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for parsing and updating the uninstall.log
+
+/**
+ * Updates the uninstall.log with new files added by software update.
+ *
+ * When modifying this macro be aware that LineFind uses all registers except
+ * $R0-$R3 and TextCompareNoDetails uses all registers except $R0-$R9 so be
+ * cautious. Callers of this macro are not affected.
+ */
+!macro UpdateUninstallLog
+
+ !ifndef UpdateUninstallLog
+ !insertmacro FileJoin
+ !insertmacro LineFind
+ !insertmacro TextCompareNoDetails
+ !insertmacro TrimNewLines
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define UpdateUninstallLog "!insertmacro UpdateUninstallLogCall"
+
+ Function UpdateUninstallLog
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+
+ ClearErrors
+
+ GetFullPathName $R3 "$INSTDIR\uninstall"
+ ${If} ${FileExists} "$R3\uninstall.update"
+ ${LineFind} "$R3\uninstall.update" "" "1:-1" "CleanupUpdateLog"
+
+ GetTempFileName $R2 "$R3"
+ FileOpen $R1 "$R2" w
+ ${TextCompareNoDetails} "$R3\uninstall.update" "$R3\uninstall.log" "SlowDiff" "CreateUpdateDiff"
+ FileClose $R1
+
+ IfErrors +2 0
+ ${FileJoin} "$R3\uninstall.log" "$R2" "$R3\uninstall.log"
+
+ ${DeleteFile} "$R2"
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ FunctionEnd
+
+ ; This callback MUST use labels vs. relative line numbers.
+ Function CleanupUpdateLog
+ StrCpy $R2 "$R9" 12
+ StrCmp "$R2" "EXECUTE ADD " +1 skip
+ StrCpy $R9 "$R9" "" 12
+
+ Push $R6
+ Push $R5
+ Push $R4
+ StrCpy $R4 "" ; Initialize to an empty string.
+ StrCpy $R6 -1 ; Set the counter to -1 so it will start at 0.
+
+ loop:
+ IntOp $R6 $R6 + 1 ; Increment the counter.
+ StrCpy $R5 $R9 1 $R6 ; Starting from the counter copy the next char.
+ StrCmp $R5 "" copy ; Are there no more chars?
+ StrCmp $R5 "/" +1 +2 ; Is the char a /?
+ StrCpy $R5 "\" ; Replace the char with a \.
+
+ StrCpy $R4 "$R4$R5"
+ GoTo loop
+
+ copy:
+ StrCpy $R9 "File: \$R4"
+ Pop $R6
+ Pop $R5
+ Pop $R4
+ GoTo end
+
+ skip:
+ StrCpy $0 "SkipWrite"
+
+ end:
+ Push $0
+ FunctionEnd
+
+ Function CreateUpdateDiff
+ ${TrimNewLines} "$9" $9
+ ${If} $9 != ""
+ FileWrite $R1 "$9$\r$\n"
+ ${EndIf}
+
+ Push 0
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro UpdateUninstallLogCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call UpdateUninstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Copies files from a source directory to a destination directory with logging
+ * to the uninstall.log. If any destination files are in use a reboot will be
+ * necessary to complete the installation and the reboot flag (see IfRebootFlag
+ * in the NSIS documentation).
+ *
+ * @param _PATH_TO_SOURCE
+ * Source path to copy the files from. This must not end with a \.
+ *
+ * @param _PATH_TO_DESTINATION
+ * Destination path to copy the files to. This must not end with a \.
+ *
+ * @param _PREFIX_ERROR_CREATEDIR
+ * Prefix for the directory creation error message. The directory path
+ * will be inserted below this string.
+ *
+ * @param _SUFFIX_ERROR_CREATEDIR
+ * Suffix for the directory creation error message. The directory path
+ * will be inserted above this string.
+ *
+ * $0 = destination file's parent directory used in the create_dir label
+ * $R0 = copied value from $R6 (e.g. _PATH_TO_SOURCE)
+ * $R1 = copied value from $R7 (e.g. _PATH_TO_DESTINATION)
+ * $R2 = string length of the path to source
+ * $R3 = relative path from the path to source
+ * $R4 = copied value from $R8 (e.g. _PREFIX_ERROR_CREATEDIR)
+ * $R5 = copied value from $R9 (e.g. _SUFFIX_ERROR_CREATEDIR)
+ * note: the LocateNoDetails macro uses these registers so we copy the values
+ * to other registers.
+ * $R6 = initially _PATH_TO_SOURCE and then set to "size" ($R6="" if directory,
+ * $R6="0" if file with /S=)"path\name" in callback
+ * $R7 = initially _PATH_TO_DESTINATION and then set to "name" in callback
+ * $R8 = initially _PREFIX_ERROR_CREATEDIR and then set to "path" in callback
+ * $R9 = initially _SUFFIX_ERROR_CREATEDIR and then set to "path\name" in
+ * callback
+ */
+!macro CopyFilesFromDir
+
+ !ifndef CopyFilesFromDir
+ !insertmacro LocateNoDetails
+ !insertmacro OnEndCommon
+ !insertmacro WordReplace
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define CopyFilesFromDir "!insertmacro CopyFilesFromDirCall"
+
+ Function CopyFilesFromDir
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+ Push $0
+
+ StrCpy $R0 "$R6"
+ StrCpy $R1 "$R7"
+ StrCpy $R4 "$R8"
+ StrCpy $R5 "$R9"
+
+ StrLen $R2 "$R0"
+
+ ${LocateNoDetails} "$R0" "/L=FD" "CopyFileCallback"
+
+ Pop $0
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ Function CopyFileCallback
+ StrCpy $R3 $R8 "" $R2 ; $R3 always begins with a \.
+
+ retry:
+ ClearErrors
+ StrCmp $R6 "" +1 copy_file
+ IfFileExists "$R1$R3\$R7" end +1
+ StrCpy $0 "$R1$R3\$R7"
+
+ create_dir:
+ ClearErrors
+ CreateDirectory "$0"
+ IfFileExists "$0" +1 err_create_dir ; protect against looping.
+ ${LogMsg} "Created Directory: $0"
+ StrCmp $R6 "" end copy_file
+
+ err_create_dir:
+ ${LogMsg} "** ERROR Creating Directory: $0 **"
+ MessageBox MB_RETRYCANCEL|MB_ICONQUESTION "$R4$\r$\n$\r$\n$0$\r$\n$\r$\n$R5" IDRETRY retry
+ ${OnEndCommon}
+ Quit
+
+ copy_file:
+ StrCpy $0 "$R1$R3"
+ StrCmp "$0" "$INSTDIR" +2 +1
+ IfFileExists "$0" +1 create_dir
+
+ ClearErrors
+ ${DeleteFile} "$R1$R3\$R7"
+ IfErrors +1 dest_clear
+ ClearErrors
+ Rename "$R1$R3\$R7" "$R1$R3\$R7.moz-delete"
+ IfErrors +1 reboot_delete
+
+ ; file will replace destination file on reboot
+ Rename "$R9" "$R9.moz-upgrade"
+ CopyFiles /SILENT "$R9.moz-upgrade" "$R1$R3"
+ Rename /REBOOTOK "$R1$R3\$R7.moz-upgrade" "$R1$R3\$R7"
+ ${LogMsg} "Copied File: $R1$R3\$R7.moz-upgrade"
+ ${LogMsg} "Delayed Install File (Reboot Required): $R1$R3\$R7"
+ GoTo log_uninstall
+
+ ; file will be deleted on reboot
+ reboot_delete:
+ CopyFiles /SILENT $R9 "$R1$R3"
+ Delete /REBOOTOK "$R1$R3\$R7.moz-delete"
+ ${LogMsg} "Installed File: $R1$R3\$R7"
+ ${LogMsg} "Delayed Delete File (Reboot Required): $R1$R3\$R7.moz-delete"
+ GoTo log_uninstall
+
+ ; destination file doesn't exist - coast is clear
+ dest_clear:
+ CopyFiles /SILENT $R9 "$R1$R3"
+ ${LogMsg} "Installed File: $R1$R3\$R7"
+
+ log_uninstall:
+ ; If the file is installed into the installation directory remove the
+ ; installation directory's path from the file path when writing to the
+ ; uninstall.log so it will be a relative path. This allows the same
+ ; helper.exe to be used with zip builds if we supply an uninstall.log.
+ ${WordReplace} "$R1$R3\$R7" "$INSTDIR" "" "+" $R3
+ ${LogUninstall} "File: $R3"
+
+ end:
+ Push 0
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CopyFilesFromDirCall _PATH_TO_SOURCE _PATH_TO_DESTINATION \
+ _PREFIX_ERROR_CREATEDIR _SUFFIX_ERROR_CREATEDIR
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_PATH_TO_SOURCE}"
+ Push "${_PATH_TO_DESTINATION}"
+ Push "${_PREFIX_ERROR_CREATEDIR}"
+ Push "${_SUFFIX_ERROR_CREATEDIR}"
+ Call CopyFilesFromDir
+ !verbose pop
+!macroend
+
+/**
+ * Parses the uninstall.log on install to first remove a previous installation's
+ * files and then their directories if empty prior to installing.
+ *
+ * When modifying this macro be aware that LineFind uses all registers except
+ * $R0-$R3 so be cautious. Callers of this macro are not affected.
+ */
+!macro OnInstallUninstall
+
+ !ifndef OnInstallUninstall
+ !insertmacro GetParent
+ !insertmacro LineFind
+ !insertmacro TrimNewLines
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define OnInstallUninstall "!insertmacro OnInstallUninstallCall"
+
+ Function OnInstallUninstall
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+ Push $TmpVal
+
+ IfFileExists "$INSTDIR\uninstall\uninstall.log" +1 end
+
+ ${LogHeader} "Removing Previous Installation"
+
+ ; Copy the uninstall log file to a temporary file
+ GetTempFileName $TmpVal
+ CopyFiles /SILENT /FILESONLY "$INSTDIR\uninstall\uninstall.log" "$TmpVal"
+
+ ; Delete files
+ ${LineFind} "$TmpVal" "/NUL" "1:-1" "RemoveFilesCallback"
+
+ ; Remove empty directories
+ ${LineFind} "$TmpVal" "/NUL" "1:-1" "RemoveDirsCallback"
+
+ ; Delete the temporary uninstall log file
+ Delete /REBOOTOK "$TmpVal"
+
+ ; Delete the uninstall log file
+ Delete "$INSTDIR\uninstall\uninstall.log"
+
+ end:
+ ClearErrors
+
+ Pop $TmpVal
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Pop $R9
+ FunctionEnd
+
+ Function RemoveFilesCallback
+ ${TrimNewLines} "$R9" $R9
+ StrCpy $R1 "$R9" 5 ; Copy the first five chars
+
+ StrCmp "$R1" "File:" +1 end
+ StrCpy $R9 "$R9" "" 6 ; Copy string starting after the 6th char
+ StrCpy $R0 "$R9" 1 ; Copy the first char
+
+ StrCmp "$R0" "\" +1 end ; If this isn't a relative path goto end
+ StrCmp "$R9" "\install.log" end +1 ; Skip the install.log
+ StrCmp "$R9" "\MapiProxy_InUse.dll" end +1 ; Skip the MapiProxy_InUse.dll
+ StrCmp "$R9" "\mozMapi32_InUse.dll" end +1 ; Skip the mozMapi32_InUse.dll
+
+ StrCpy $R1 "$INSTDIR$R9" ; Copy the install dir path and suffix it with the string
+ IfFileExists "$R1" +1 end
+
+ ClearErrors
+ Delete "$R1"
+ ${Unless} ${Errors}
+ ${LogMsg} "Deleted File: $R1"
+ Goto end
+ ${EndUnless}
+
+ ClearErrors
+ Rename "$R1" "$R1.moz-delete"
+ ${Unless} ${Errors}
+ Delete /REBOOTOK "$R1.moz-delete"
+ ${LogMsg} "Delayed Delete File (Reboot Required): $R1.moz-delete"
+ GoTo end
+ ${EndUnless}
+
+ ; Check if the file exists in the source. If it does the new file will
+ ; replace the existing file when the system is rebooted. If it doesn't
+ ; the file will be deleted when the system is rebooted.
+ ${Unless} ${FileExists} "$EXEDIR\core$R9"
+ ${AndUnless} ${FileExists} "$EXEDIR\optional$R9"
+ Delete /REBOOTOK "$R1"
+ ${LogMsg} "Delayed Delete File (Reboot Required): $R1"
+ ${EndUnless}
+
+ end:
+ ClearErrors
+
+ Push 0
+ FunctionEnd
+
+ ; Using locate will leave file handles open to some of the directories
+ ; which will prevent the deletion of these directories. This parses the
+ ; uninstall.log and uses the file entries to find / remove empty
+ ; directories.
+ Function RemoveDirsCallback
+ ${TrimNewLines} "$R9" $R9
+ StrCpy $R0 "$R9" 5 ; Copy the first five chars
+ StrCmp "$R0" "File:" +1 end
+
+ StrCpy $R9 "$R9" "" 6 ; Copy string starting after the 6th char
+ StrCpy $R0 "$R9" 1 ; Copy the first char
+
+ StrCpy $R1 "$INSTDIR$R9" ; Copy the install dir path and suffix it with the string
+ StrCmp "$R0" "\" loop end ; If this isn't a relative path goto end
+
+ loop:
+ ${GetParent} "$R1" $R1 ; Get the parent directory for the path
+ StrCmp "$R1" "$INSTDIR" end +1 ; If the directory is the install dir goto end
+
+ IfFileExists "$R1" +1 loop ; Only try to remove the dir if it exists
+ ClearErrors
+ RmDir "$R1" ; Remove the dir
+ IfErrors end +1 ; If we fail there is no use trying to remove its parent dir
+ ${LogMsg} "Deleted Directory: $R1"
+ GoTo loop
+
+ end:
+ ClearErrors
+
+ Push 0
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro OnInstallUninstallCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call OnInstallUninstall
+ !verbose pop
+!macroend
+
+/**
+ * Parses the precomplete file to remove an installation's files and
+ * directories.
+ *
+ * @param _CALLBACK
+ * The function address of a callback function for progress or "false"
+ * if there is no callback function.
+ *
+ * $R3 = false if all files were deleted or moved to the tobedeleted directory.
+ * true if file(s) could not be moved to the tobedeleted directory.
+ * $R4 = Path to temporary precomplete file.
+ * $R5 = File handle for the temporary precomplete file.
+ * $R6 = String returned from FileRead.
+ * $R7 = First seven characters of the string returned from FileRead.
+ * $R8 = Temporary file path used to rename files that are in use.
+ * $R9 = _CALLBACK
+ */
+!macro RemovePrecompleteEntries
+
+ !ifndef ${_MOZFUNC_UN}RemovePrecompleteEntries
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParent
+ !insertmacro ${_MOZFUNC_UN_TMP}TrimNewLines
+ !insertmacro ${_MOZFUNC_UN_TMP}WordReplace
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}RemovePrecompleteEntries "!insertmacro ${_MOZFUNC_UN}RemovePrecompleteEntriesCall"
+
+ Function ${_MOZFUNC_UN}RemovePrecompleteEntries
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+
+ ${If} ${FileExists} "$INSTDIR\precomplete"
+ StrCpy $R3 "false"
+
+ RmDir /r "$INSTDIR\${TO_BE_DELETED}"
+ CreateDirectory "$INSTDIR\${TO_BE_DELETED}"
+ GetTempFileName $R4 "$INSTDIR\${TO_BE_DELETED}"
+ Delete "$R4"
+ Rename "$INSTDIR\precomplete" "$R4"
+
+ ClearErrors
+ ; Rename and then remove files
+ FileOpen $R5 "$R4" r
+ ${Do}
+ FileRead $R5 $R6
+ ${If} ${Errors}
+ ${Break}
+ ${EndIf}
+
+ ${${_MOZFUNC_UN}TrimNewLines} "$R6" $R6
+ ; Replace all occurrences of "/" with "\".
+ ${${_MOZFUNC_UN}WordReplace} "$R6" "/" "\" "+" $R6
+
+ ; Copy the first 7 chars
+ StrCpy $R7 "$R6" 7
+ ${If} "$R7" == "remove "
+ ; Copy the string starting after the 8th char
+ StrCpy $R6 "$R6" "" 8
+ ; Copy all but the last char to remove the double quote.
+ StrCpy $R6 "$R6" -1
+ ${If} ${FileExists} "$INSTDIR\$R6"
+ ${Unless} "$R9" == "false"
+ Call $R9
+ ${EndUnless}
+
+ ClearErrors
+ Delete "$INSTDIR\$R6"
+ ${If} ${Errors}
+ GetTempFileName $R8 "$INSTDIR\${TO_BE_DELETED}"
+ Delete "$R8"
+ ClearErrors
+ Rename "$INSTDIR\$R6" "$R8"
+ ${Unless} ${Errors}
+ Delete /REBOOTOK "$R8"
+
+ ClearErrors
+ ${EndUnless}
+!ifdef __UNINSTALL__
+ ${If} ${Errors}
+ Delete /REBOOTOK "$INSTDIR\$R6"
+ StrCpy $R3 "true"
+ ClearErrors
+ ${EndIf}
+!endif
+ ${EndIf}
+ ${EndIf}
+ ${ElseIf} "$R7" == "rmdir $\""
+ ; Copy the string starting after the 7th char.
+ StrCpy $R6 "$R6" "" 7
+ ; Copy all but the last two chars to remove the slash and the double quote.
+ StrCpy $R6 "$R6" -2
+ ${If} ${FileExists} "$INSTDIR\$R6"
+ ; Ignore directory removal errors
+ RmDir "$INSTDIR\$R6"
+ ClearErrors
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+ FileClose $R5
+
+ ; Delete the temporary precomplete file
+ Delete /REBOOTOK "$R4"
+
+ RmDir /r /REBOOTOK "$INSTDIR\${TO_BE_DELETED}"
+
+ ${If} ${RebootFlag}
+ ${AndIf} "$R3" == "false"
+ ; Clear the reboot flag if all files were deleted or moved to the
+ ; tobedeleted directory.
+ SetRebootFlag false
+ ${EndIf}
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro RemovePrecompleteEntriesCall _CALLBACK
+ !verbose push
+ Push "${_CALLBACK}"
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call RemovePrecompleteEntries
+ !verbose pop
+!macroend
+
+!macro un.RemovePrecompleteEntriesCall _CALLBACK
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_CALLBACK}"
+ Call un.RemovePrecompleteEntries
+ !verbose pop
+!macroend
+
+!macro un.RemovePrecompleteEntries
+ !ifndef un.RemovePrecompleteEntries
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro RemovePrecompleteEntries
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Parses the uninstall.log to unregister dll's, remove files, and remove
+ * empty directories for this installation.
+ *
+ * When modifying this macro be aware that LineFind uses all registers except
+ * $R0-$R3 so be cautious. Callers of this macro are not affected.
+ */
+!macro un.ParseUninstallLog
+
+ !ifndef un.ParseUninstallLog
+ !insertmacro un.GetParent
+ !insertmacro un.LineFind
+ !insertmacro un.TrimNewLines
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define un.ParseUninstallLog "!insertmacro un.ParseUninstallLogCall"
+
+ Function un.ParseUninstallLog
+ Push $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+ Push $TmpVal
+
+ IfFileExists "$INSTDIR\uninstall\uninstall.log" +1 end
+
+ ; Copy the uninstall log file to a temporary file
+ GetTempFileName $TmpVal
+ CopyFiles /SILENT /FILESONLY "$INSTDIR\uninstall\uninstall.log" "$TmpVal"
+
+ ; Unregister DLL's
+ ${un.LineFind} "$TmpVal" "/NUL" "1:-1" "un.UnRegDLLsCallback"
+
+ ; Delete files
+ ${un.LineFind} "$TmpVal" "/NUL" "1:-1" "un.RemoveFilesCallback"
+
+ ; Remove empty directories
+ ${un.LineFind} "$TmpVal" "/NUL" "1:-1" "un.RemoveDirsCallback"
+
+ ; Delete the temporary uninstall log file
+ Delete /REBOOTOK "$TmpVal"
+
+ end:
+
+ Pop $TmpVal
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Pop $R9
+ FunctionEnd
+
+ Function un.RemoveFilesCallback
+ ${un.TrimNewLines} "$R9" $R9
+ StrCpy $R1 "$R9" 5
+
+ StrCmp "$R1" "File:" +1 end
+ StrCpy $R9 "$R9" "" 6
+ StrCpy $R0 "$R9" 1
+
+ StrCpy $R1 "$INSTDIR$R9"
+ StrCmp "$R0" "\" +2 +1
+ StrCpy $R1 "$R9"
+
+ IfFileExists "$R1" +1 end
+ Delete "$R1"
+ IfErrors +1 end
+ ClearErrors
+ Rename "$R1" "$R1.moz-delete"
+ IfErrors +1 +3
+ Delete /REBOOTOK "$R1"
+ GoTo end
+
+ Delete /REBOOTOK "$R1.moz-delete"
+
+ end:
+ ClearErrors
+
+ Push 0
+ FunctionEnd
+
+ Function un.UnRegDLLsCallback
+ ${un.TrimNewLines} "$R9" $R9
+ StrCpy $R1 "$R9" 7
+
+ StrCmp $R1 "DLLReg:" +1 end
+ StrCpy $R9 "$R9" "" 8
+ StrCpy $R0 "$R9" 1
+
+ StrCpy $R1 "$INSTDIR$R9"
+ StrCmp $R0 "\" +2 +1
+ StrCpy $R1 "$R9"
+
+ ${UnregisterDLL} $R1
+
+ end:
+ ClearErrors
+
+ Push 0
+ FunctionEnd
+
+ ; Using locate will leave file handles open to some of the directories
+ ; which will prevent the deletion of these directories. This parses the
+ ; uninstall.log and uses the file entries to find / remove empty
+ ; directories.
+ Function un.RemoveDirsCallback
+ ${un.TrimNewLines} "$R9" $R9
+ StrCpy $R0 "$R9" 5 ; Copy the first five chars
+ StrCmp "$R0" "File:" +1 end
+
+ StrCpy $R9 "$R9" "" 6 ; Copy string starting after the 6th char
+ StrCpy $R0 "$R9" 1 ; Copy the first char
+
+ StrCpy $R1 "$INSTDIR$R9" ; Copy the install dir path and suffix it with the string
+ StrCmp "$R0" "\" loop ; If this is a relative path goto the loop
+ StrCpy $R1 "$R9" ; Already a full path so copy the string
+
+ loop:
+ ${un.GetParent} "$R1" $R1 ; Get the parent directory for the path
+ StrCmp "$R1" "$INSTDIR" end ; If the directory is the install dir goto end
+
+ ; We only try to remove empty directories but the Desktop, StartMenu, and
+ ; QuickLaunch directories can be empty so guard against removing them.
+ SetShellVarContext all ; Set context to all users
+ StrCmp "$R1" "$DESKTOP" end ; All users desktop
+ StrCmp "$R1" "$STARTMENU" end ; All users start menu
+
+ SetShellVarContext current ; Set context to all users
+ StrCmp "$R1" "$DESKTOP" end ; Current user desktop
+ StrCmp "$R1" "$STARTMENU" end ; Current user start menu
+ StrCmp "$R1" "$QUICKLAUNCH" end ; Current user quick launch
+
+ IfFileExists "$R1" +1 +3 ; Only try to remove the dir if it exists
+ ClearErrors
+ RmDir "$R1" ; Remove the dir
+ IfErrors end ; If we fail there is no use trying to remove its parent dir
+
+ StrCmp "$R0" "\" loop end ; Only loop when the path is relative to the install dir
+
+ end:
+ ClearErrors
+
+ Push 0
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro un.ParseUninstallLogCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.ParseUninstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Finds a valid Start Menu shortcut in the uninstall log and returns the
+ * relative path from the Start Menu's Programs directory to the shortcut's
+ * directory.
+ *
+ * When modifying this macro be aware that LineFind uses all registers except
+ * $R0-$R3 so be cautious. Callers of this macro are not affected.
+ *
+ * @return _REL_PATH_TO_DIR
+ * The relative path to the application's Start Menu directory from the
+ * Start Menu's Programs directory.
+ */
+!macro FindSMProgramsDir
+
+ !ifndef FindSMProgramsDir
+ !insertmacro GetParent
+ !insertmacro LineFind
+ !insertmacro TrimNewLines
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define FindSMProgramsDir "!insertmacro FindSMProgramsDirCall"
+
+ Function FindSMProgramsDir
+ Exch $R3
+ Push $R2
+ Push $R1
+ Push $R0
+
+ StrCpy $R3 ""
+ ${If} ${FileExists} "$INSTDIR\uninstall\uninstall.log"
+ ${LineFind} "$INSTDIR\uninstall\uninstall.log" "/NUL" "1:-1" "FindSMProgramsDirRelPath"
+ ${EndIf}
+ ClearErrors
+
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Exch $R3
+ FunctionEnd
+
+ ; This callback MUST use labels vs. relative line numbers.
+ Function FindSMProgramsDirRelPath
+ Push 0
+ ${TrimNewLines} "$R9" $R9
+ StrCpy $R4 "$R9" 5
+
+ StrCmp "$R4" "File:" +1 end_FindSMProgramsDirRelPath
+ StrCpy $R9 "$R9" "" 6
+ StrCpy $R4 "$R9" 1
+
+ StrCmp "$R4" "\" end_FindSMProgramsDirRelPath +1
+
+ SetShellVarContext all
+ ${GetLongPath} "$SMPROGRAMS" $R4
+ StrLen $R2 "$R4"
+ StrCpy $R1 "$R9" $R2
+ StrCmp "$R1" "$R4" +1 end_FindSMProgramsDirRelPath
+ IfFileExists "$R9" +1 end_FindSMProgramsDirRelPath
+ ShellLink::GetShortCutTarget "$R9"
+ Pop $R0
+ StrCmp "$INSTDIR\${FileMainEXE}" "$R0" +1 end_FindSMProgramsDirRelPath
+ ${GetParent} "$R9" $R3
+ IntOp $R2 $R2 + 1
+ StrCpy $R3 "$R3" "" $R2
+
+ Pop $R4 ; Remove the previously pushed 0 from the stack and
+ push "StopLineFind" ; push StopLineFind to stop finding more lines.
+
+ end_FindSMProgramsDirRelPath:
+ ClearErrors
+
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro FindSMProgramsDirCall _REL_PATH_TO_DIR
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call FindSMProgramsDir
+ Pop ${_REL_PATH_TO_DIR}
+ !verbose pop
+!macroend
+
+
+################################################################################
+# Macros for custom branding
+
+/**
+ * Sets BrandFullName and / or BrandShortName to values provided in the specified
+ * ini file and defaults to BrandShortName and BrandFullName as defined in
+ * branding.nsi when the associated ini file entry is not specified.
+ *
+ * ini file format:
+ * [Branding]
+ * BrandFullName=Custom Full Name
+ * BrandShortName=Custom Short Name
+ *
+ * @param _PATH_TO_INI
+ * Path to the ini file.
+ *
+ * $R6 = return value from ReadINIStr
+ * $R7 = stores BrandShortName
+ * $R8 = stores BrandFullName
+ * $R9 = _PATH_TO_INI
+ */
+!macro SetBrandNameVars
+
+ !ifndef ${_MOZFUNC_UN}SetBrandNameVars
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}WordReplace
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ ; Prevent declaring vars twice when the SetBrandNameVars macro is
+ ; inserted into both the installer and uninstaller.
+ !ifndef SetBrandNameVars
+ Var BrandFullName
+ Var BrandFullNameDA
+ Var BrandShortName
+ !endif
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}SetBrandNameVars "!insertmacro ${_MOZFUNC_UN}SetBrandNameVarsCall"
+
+ Function ${_MOZFUNC_UN}SetBrandNameVars
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+
+ StrCpy $R8 "${BrandFullName}"
+ StrCpy $R7 "${BrandShortName}"
+
+ IfFileExists "$R9" +1 finish
+
+ ClearErrors
+ ReadINIStr $R6 $R9 "Branding" "BrandFullName"
+ IfErrors +2 +1
+ StrCpy $R8 "$R6"
+
+ ClearErrors
+ ReadINIStr $R6 $R9 "Branding" "BrandShortName"
+ IfErrors +2 +1
+ StrCpy $R7 "$R6"
+
+ finish:
+ StrCpy $BrandFullName "$R8"
+ ${${_MOZFUNC_UN}WordReplace} "$R8" "&" "&&" "+" $R8
+ StrCpy $BrandFullNameDA "$R8"
+ StrCpy $BrandShortName "$R7"
+
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro SetBrandNameVarsCall _PATH_TO_INI
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_PATH_TO_INI}"
+ Call SetBrandNameVars
+ !verbose pop
+!macroend
+
+!macro un.SetBrandNameVarsCall _PATH_TO_INI
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_PATH_TO_INI}"
+ Call un.SetBrandNameVars
+ !verbose pop
+!macroend
+
+!macro un.SetBrandNameVars
+ !ifndef un.SetBrandNameVars
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro SetBrandNameVars
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Replaces the wizard's header image with the one specified.
+ *
+ * @param _PATH_TO_IMAGE
+ * Fully qualified path to the bitmap to use for the header image.
+ *
+ * $R8 = hwnd for the control returned from GetDlgItem.
+ * $R9 = _PATH_TO_IMAGE
+ */
+!macro ChangeMUIHeaderImage
+
+ !ifndef ${_MOZFUNC_UN}ChangeMUIHeaderImage
+ Var hHeaderBitmap
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}ChangeMUIHeaderImage "!insertmacro ${_MOZFUNC_UN}ChangeMUIHeaderImageCall"
+
+ Function ${_MOZFUNC_UN}ChangeMUIHeaderImage
+ Exch $R9
+ Push $R8
+
+ GetDlgItem $R8 $HWNDPARENT 1046
+ System::Call 'user32::LoadImageW(i 0, w "$R9", i 0, i 0, i 0, i 0x0010|0x2000) i.s'
+ Pop $hHeaderBitmap
+ SendMessage $R8 ${STM_SETIMAGE} 0 $hHeaderBitmap
+ ; There is no way to specify a show function for a custom page so hide
+ ; and then show the control to force the bitmap to redraw.
+ ShowWindow $R8 ${SW_HIDE}
+ ShowWindow $R8 ${SW_SHOW}
+
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro ChangeMUIHeaderImageCall _PATH_TO_IMAGE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_PATH_TO_IMAGE}"
+ Call ChangeMUIHeaderImage
+ !verbose pop
+!macroend
+
+!macro un.ChangeMUIHeaderImageCall _PATH_TO_IMAGE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_PATH_TO_IMAGE}"
+ Call un.ChangeMUIHeaderImage
+ !verbose pop
+!macroend
+
+!macro un.ChangeMUIHeaderImage
+ !ifndef un.ChangeMUIHeaderImage
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro ChangeMUIHeaderImage
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# User interface callback helper defines and macros
+
+/* Install type defines */
+!ifndef INSTALLTYPE_BASIC
+ !define INSTALLTYPE_BASIC 1
+!endif
+
+!ifndef INSTALLTYPE_CUSTOM
+ !define INSTALLTYPE_CUSTOM 2
+!endif
+
+/**
+ * Checks whether to display the current page (e.g. if not performing a custom
+ * install don't display the custom pages).
+ */
+!macro CheckCustomCommon
+
+ !ifndef CheckCustomCommon
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define CheckCustomCommon "!insertmacro CheckCustomCommonCall"
+
+ Function CheckCustomCommon
+
+ ; Abort if not a custom install
+ IntCmp $InstallType ${INSTALLTYPE_CUSTOM} +2 +1 +1
+ Abort
+
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro CheckCustomCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call CheckCustomCommon
+ !verbose pop
+!macroend
+
+/**
+ * Unloads dll's and releases references when the installer and uninstaller
+ * exit.
+ */
+!macro OnEndCommon
+
+ !ifndef ${_MOZFUNC_UN}OnEndCommon
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}UnloadUAC
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}OnEndCommon "!insertmacro ${_MOZFUNC_UN}OnEndCommonCall"
+
+ Function ${_MOZFUNC_UN}OnEndCommon
+
+ ${${_MOZFUNC_UN}UnloadUAC}
+ StrCmp $hHeaderBitmap "" +3 +1
+ System::Call "gdi32::DeleteObject(i s)" $hHeaderBitmap
+ StrCpy $hHeaderBitmap ""
+
+ System::Free 0
+
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro OnEndCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call OnEndCommon
+ !verbose pop
+!macroend
+
+!macro un.OnEndCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.OnEndCommon
+ !verbose pop
+!macroend
+
+!macro un.OnEndCommon
+ !ifndef un.OnEndCommon
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro OnEndCommon
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Called from the installer's .onInit function not to be confused with the
+ * uninstaller's .onInit or the uninstaller's un.onInit functions.
+ *
+ * @param _WARN_UNSUPPORTED_MSG
+ * Message displayed when the Windows version is not supported.
+ *
+ * $R5 = return value from the GetSize macro
+ * $R6 = general string values, return value from GetTempFileName, return
+ * value from the GetSize macro
+ * $R7 = full path to the configuration ini file
+ * $R8 = used for OS Version and Service Pack detection and the return value
+ * from the GetParameters macro
+ * $R9 = _WARN_UNSUPPORTED_MSG
+ */
+!macro InstallOnInitCommon
+
+ !ifndef InstallOnInitCommon
+ !insertmacro ElevateUAC
+ !insertmacro GetOptions
+ !insertmacro GetParameters
+ !insertmacro GetSize
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define InstallOnInitCommon "!insertmacro InstallOnInitCommonCall"
+
+ Function InstallOnInitCommon
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ ; Don't install on systems that don't support SSE2. The parameter value of
+ ; 10 is for PF_XMMI64_INSTRUCTIONS_AVAILABLE which will check whether the
+ ; SSE2 instruction set is available.
+ System::Call "kernel32::IsProcessorFeaturePresent(i 10)i .R8"
+ ${If} "$R8" == "0"
+ MessageBox MB_OK|MB_ICONSTOP "$R9"
+ ; Nothing initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+
+ !ifdef HAVE_64BIT_BUILD
+ ${Unless} ${RunningX64}
+ ${OrUnless} ${AtLeastWin7}
+ MessageBox MB_OK|MB_ICONSTOP "$R9"
+ ; Nothing initialized so no need to call OnEndCommon
+ Quit
+ ${EndUnless}
+
+ SetRegView 64
+ !else
+ StrCpy $R8 "0"
+ ${If} ${AtMostWin2000}
+ StrCpy $R8 "1"
+ ${EndIf}
+
+ ${If} ${IsWinXP}
+ ${AndIf} ${AtMostServicePack} 1
+ StrCpy $R8 "1"
+ ${EndIf}
+
+ ${If} $R8 == "1"
+ ; XXX-rstrong - some systems failed the AtLeastWin2000 test that we
+ ; used to use for an unknown reason and likely fail the AtMostWin2000
+ ; and possibly the IsWinXP test as well. To work around this also
+ ; check if the Windows NT registry Key exists and if it does if the
+ ; first char in CurrentVersion is equal to 3 (Windows NT 3.5 and
+ ; 3.5.1), to 4 (Windows NT 4) or 5 (Windows 2000 and Windows XP).
+ StrCpy $R8 ""
+ ClearErrors
+ ReadRegStr $R8 HKLM "SOFTWARE\Microsoft\Windows NT\CurrentVersion" "CurrentVersion"
+ StrCpy $R8 "$R8" 1
+ ${If} ${Errors}
+ ${OrIf} "$R8" == "3"
+ ${OrIf} "$R8" == "4"
+ ${OrIf} "$R8" == "5"
+ MessageBox MB_OK|MB_ICONSTOP "$R9"
+ ; Nothing initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ${EndUnless}
+ !endif
+
+ ${GetParameters} $R8
+
+ ${If} $R8 != ""
+ ; Default install type
+ StrCpy $InstallType ${INSTALLTYPE_BASIC}
+
+ ${Unless} ${Silent}
+ ; Manually check for /S in the command line due to Bug 506867
+ ClearErrors
+ ${GetOptions} "$R8" "/S" $R7
+ ${Unless} ${Errors}
+ SetSilent silent
+ ${Else}
+ ; Support for the deprecated -ms command line argument. The new command
+ ; line arguments are not supported when -ms is used.
+ ClearErrors
+ ${GetOptions} "$R8" "-ms" $R7
+ ${Unless} ${Errors}
+ SetSilent silent
+ ${EndUnless}
+ ${EndUnless}
+ ${EndUnless}
+
+ ; Support for specifying an installation configuration file.
+ ClearErrors
+ ${GetOptions} "$R8" "/INI=" $R7
+ ${Unless} ${Errors}
+ ; The configuration file must also exist
+ ${If} ${FileExists} "$R7"
+ SetSilent silent
+ ReadINIStr $R8 $R7 "Install" "InstallDirectoryName"
+ ${If} $R8 != ""
+ !ifdef HAVE_64BIT_BUILD
+ StrCpy $INSTDIR "$PROGRAMFILES64\$R8"
+ !else
+ StrCpy $INSTDIR "$PROGRAMFILES32\$R8"
+ !endif
+ ${Else}
+ ReadINIStr $R8 $R7 "Install" "InstallDirectoryPath"
+ ${If} $R8 != ""
+ StrCpy $INSTDIR "$R8"
+ ${EndIf}
+ ${EndIf}
+
+ ; Quit if we are unable to create the installation directory or we are
+ ; unable to write to a file in the installation directory.
+ ClearErrors
+ ${If} ${FileExists} "$INSTDIR"
+ GetTempFileName $R6 "$INSTDIR"
+ FileOpen $R5 "$R6" w
+ FileWrite $R5 "Write Access Test"
+ FileClose $R5
+ Delete $R6
+ ${If} ${Errors}
+ ; Attempt to elevate and then try again.
+ ${ElevateUAC}
+ GetTempFileName $R6 "$INSTDIR"
+ FileOpen $R5 "$R6" w
+ FileWrite $R5 "Write Access Test"
+ FileClose $R5
+ Delete $R6
+ ${If} ${Errors}
+ ; Nothing initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ${EndIf}
+ ${Else}
+ CreateDirectory "$INSTDIR"
+ ${If} ${Errors}
+ ; Attempt to elevate and then try again.
+ ${ElevateUAC}
+ CreateDirectory "$INSTDIR"
+ ${If} ${Errors}
+ ; Nothing initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+
+ ReadINIStr $R8 $R7 "Install" "QuickLaunchShortcut"
+ ${If} $R8 == "false"
+ StrCpy $AddQuickLaunchSC "0"
+ ${Else}
+ StrCpy $AddQuickLaunchSC "1"
+ ${EndIf}
+
+ ReadINIStr $R8 $R7 "Install" "DesktopShortcut"
+ ${If} $R8 == "false"
+ StrCpy $AddDesktopSC "0"
+ ${Else}
+ StrCpy $AddDesktopSC "1"
+ ${EndIf}
+
+ ReadINIStr $R8 $R7 "Install" "StartMenuShortcuts"
+ ${If} $R8 == "false"
+ StrCpy $AddStartMenuSC "0"
+ ${Else}
+ StrCpy $AddStartMenuSC "1"
+ ${EndIf}
+
+ ReadINIStr $R8 $R7 "Install" "TaskbarShortcut"
+ ${If} $R8 == "false"
+ StrCpy $AddTaskbarSC "0"
+ ${Else}
+ StrCpy $AddTaskbarSC "1"
+ ${EndIf}
+
+ ReadINIStr $R8 $R7 "Install" "MaintenanceService"
+ ${If} $R8 == "false"
+ StrCpy $InstallMaintenanceService "0"
+ ${Else}
+ ; Installing the service always requires elevation.
+ ${ElevateUAC}
+ ${EndIf}
+
+ !ifndef NO_STARTMENU_DIR
+ ReadINIStr $R8 $R7 "Install" "StartMenuDirectoryName"
+ ${If} $R8 != ""
+ StrCpy $StartMenuDir "$R8"
+ ${EndIf}
+ !endif
+ ${EndIf}
+ ${Else}
+ ; If this isn't an INI install, we need to try to elevate now.
+ ; We'll check the user's permission level later on to determine the
+ ; default install path (which will be the real install path for /S).
+ ; If an INI file is used, we try to elevate down that path when needed.
+ ${ElevateUAC}
+ ${EndUnless}
+ ${EndIf}
+ ClearErrors
+
+ ${IfNot} ${Silent}
+ ${ElevateUAC}
+ ${EndIf}
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro InstallOnInitCommonCall _WARN_UNSUPPORTED_MSG
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_WARN_UNSUPPORTED_MSG}"
+ Call InstallOnInitCommon
+ !verbose pop
+!macroend
+
+/**
+ * Called from the uninstaller's .onInit function not to be confused with the
+ * installer's .onInit or the uninstaller's un.onInit functions.
+ */
+!macro UninstallOnInitCommon
+
+ !ifndef UninstallOnInitCommon
+ !insertmacro ElevateUAC
+ !insertmacro GetLongPath
+ !insertmacro GetOptions
+ !insertmacro GetParameters
+ !insertmacro GetParent
+ !insertmacro UnloadUAC
+ !insertmacro UpdateShortcutAppModelIDs
+ !insertmacro UpdateUninstallLog
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define UninstallOnInitCommon "!insertmacro UninstallOnInitCommonCall"
+
+ Function UninstallOnInitCommon
+ ; Prevents breaking apps that don't use SetBrandNameVars
+ !ifdef SetBrandNameVars
+ ${SetBrandNameVars} "$EXEDIR\distribution\setup.ini"
+ !endif
+
+ ; Prevent launching the application when a reboot is required and this
+ ; executable is the main application executable
+ IfFileExists "$EXEDIR\${FileMainEXE}.moz-upgrade" +1 +4
+ MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +2
+ Reboot
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ ${GetParent} "$EXEDIR" $INSTDIR
+ ${GetLongPath} "$INSTDIR" $INSTDIR
+ IfFileExists "$INSTDIR\${FileMainEXE}" +2 +1
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+!ifmacrodef InitHashAppModelId
+ ; setup the application model id registration value
+ !ifdef AppName
+ ${InitHashAppModelId} "$INSTDIR" "Software\Mozilla\${AppName}\TaskBarIDs"
+ !endif
+!endif
+
+ ; Prevents breaking apps that don't use SetBrandNameVars
+ !ifdef SetBrandNameVars
+ ${SetBrandNameVars} "$INSTDIR\distribution\setup.ini"
+ !endif
+
+ ; Application update uses a directory named tobedeleted in the $INSTDIR to
+ ; delete files on OS reboot when they are in use. Try to delete this
+ ; directory if it exists.
+ ${If} ${FileExists} "$INSTDIR\${TO_BE_DELETED}"
+ RmDir /r "$INSTDIR\${TO_BE_DELETED}"
+ ${EndIf}
+
+ ; Prevent all operations (e.g. set as default, postupdate, etc.) when a
+ ; reboot is required and the executable launched is helper.exe
+ IfFileExists "$INSTDIR\${FileMainEXE}.moz-upgrade" +1 +4
+ MessageBox MB_YESNO|MB_ICONEXCLAMATION "$(WARN_RESTART_REQUIRED_UPGRADE)" IDNO +2
+ Reboot
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 64
+ !endif
+
+ ${GetParameters} $R0
+
+ StrCmp "$R0" "" continue +1
+
+ ; Update this user's shortcuts with the latest app user model id.
+ ClearErrors
+ ${GetOptions} "$R0" "/UpdateShortcutAppUserModelIds" $R2
+ IfErrors hideshortcuts +1
+ StrCpy $R2 ""
+!ifmacrodef InitHashAppModelId
+ ${If} "$AppUserModelID" != ""
+ ${UpdateShortcutAppModelIDs} "$INSTDIR\${FileMainEXE}" "$AppUserModelID" $R2
+ ${EndIf}
+!endif
+ StrCmp "$R2" "false" +1 finish ; true indicates that shortcuts have been updated
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ ; Require elevation if the user can elevate
+ hideshortcuts:
+ ClearErrors
+ ${GetOptions} "$R0" "/HideShortcuts" $R2
+ IfErrors showshortcuts +1
+!ifndef NONADMIN_ELEVATE
+ ${ElevateUAC}
+!endif
+ ${HideShortcuts}
+ GoTo finish
+
+ ; Require elevation if the user can elevate
+ showshortcuts:
+ ClearErrors
+ ${GetOptions} "$R0" "/ShowShortcuts" $R2
+ IfErrors defaultappuser +1
+!ifndef NONADMIN_ELEVATE
+ ${ElevateUAC}
+!endif
+ ${ShowShortcuts}
+ GoTo finish
+
+ ; Require elevation if the the StartMenuInternet registry keys require
+ ; updating and the user can elevate
+ defaultappuser:
+ ClearErrors
+ ${GetOptions} "$R0" "/SetAsDefaultAppUser" $R2
+ IfErrors defaultappglobal +1
+ ${SetAsDefaultAppUser}
+ GoTo finish
+
+ ; Require elevation if the user can elevate
+ defaultappglobal:
+ ClearErrors
+ ${GetOptions} "$R0" "/SetAsDefaultAppGlobal" $R2
+ IfErrors postupdate +1
+ ${ElevateUAC}
+ ${SetAsDefaultAppGlobal}
+ GoTo finish
+
+ ; Do not attempt to elevate. The application launching this executable is
+ ; responsible for elevation if it is required.
+ postupdate:
+ ${WordReplace} "$R0" "$\"" "" "+" $R0
+ ClearErrors
+ ${GetOptions} "$R0" "/PostUpdate" $R2
+ IfErrors continue +1
+ ; If the uninstall.log does not exist don't perform post update
+ ; operations. This prevents updating the registry for zip builds.
+ IfFileExists "$EXEDIR\uninstall.log" +2 +1
+ Quit ; Nothing initialized so no need to call OnEndCommon
+ ${PostUpdate}
+ ClearErrors
+ ${GetOptions} "$R0" "/UninstallLog=" $R2
+ IfErrors updateuninstalllog +1
+ StrCmp "$R2" "" finish +1
+ GetFullPathName $R3 "$R2"
+ IfFileExists "$R3" +1 finish
+ Delete "$INSTDIR\uninstall\*wizard*"
+ Delete "$INSTDIR\uninstall\uninstall.log"
+ CopyFiles /SILENT /FILESONLY "$R3" "$INSTDIR\uninstall\"
+ ${GetParent} "$R3" $R4
+ Delete "$R3"
+ RmDir "$R4"
+ GoTo finish
+
+ ; Do not attempt to elevate. The application launching this executable is
+ ; responsible for elevation if it is required.
+ updateuninstalllog:
+ ${UpdateUninstallLog}
+
+ finish:
+ ${UnloadUAC}
+ System::Call "shell32::SHChangeNotify(i ${SHCNE_ASSOCCHANGED}, i 0, i 0, i 0)"
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ continue:
+
+ ; If the uninstall.log does not exist don't perform uninstall
+ ; operations. This prevents running the uninstaller for zip builds.
+ IfFileExists "$INSTDIR\uninstall\uninstall.log" +2 +1
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ ; Require elevation if the user can elevate
+ ${ElevateUAC}
+
+ ; If we made it this far then this installer is being used as an uninstaller.
+ WriteUninstaller "$EXEDIR\uninstaller.exe"
+
+ ${Unless} ${Silent}
+ ; Manually check for /S in the command line due to Bug 506867
+ ClearErrors
+ ${GetOptions} "$R0" "/S" $R2
+ ${Unless} ${Errors}
+ SetSilent silent
+ ${Else}
+ ; Support for the deprecated -ms command line argument.
+ ClearErrors
+ ${GetOptions} "$R0" "-ms" $R2
+ ${Unless} ${Errors}
+ SetSilent silent
+ ${EndUnless}
+ ${EndUnless}
+ ${EndUnless}
+
+ ${If} ${Silent}
+ StrCpy $R1 "$\"$EXEDIR\uninstaller.exe$\" /S"
+ ${Else}
+ StrCpy $R1 "$\"$EXEDIR\uninstaller.exe$\""
+ ${EndIf}
+
+ ; When the uninstaller is launched it copies itself to the temp directory
+ ; so it won't be in use so it can delete itself.
+ ExecWait $R1
+ ${DeleteFile} "$EXEDIR\uninstaller.exe"
+ ${UnloadUAC}
+ SetErrorLevel 0
+ Quit ; Nothing initialized so no need to call OnEndCommon
+
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro UninstallOnInitCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call UninstallOnInitCommon
+ !verbose pop
+!macroend
+
+/**
+ * Called from the uninstaller's un.onInit function not to be confused with the
+ * installer's .onInit or the uninstaller's .onInit functions.
+ */
+!macro un.UninstallUnOnInitCommon
+
+ !ifndef un.UninstallUnOnInitCommon
+ !insertmacro un.GetLongPath
+ !insertmacro un.GetParent
+ !insertmacro un.SetBrandNameVars
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define un.UninstallUnOnInitCommon "!insertmacro un.UninstallUnOnInitCommonCall"
+
+ Function un.UninstallUnOnInitCommon
+ ${un.GetParent} "$INSTDIR" $INSTDIR
+ ${un.GetLongPath} "$INSTDIR" $INSTDIR
+ ${Unless} ${FileExists} "$INSTDIR\${FileMainEXE}"
+ Abort
+ ${EndUnless}
+
+ !ifdef HAVE_64BIT_BUILD
+ SetRegView 64
+ !endif
+
+ ; Prevents breaking apps that don't use SetBrandNameVars
+ !ifdef un.SetBrandNameVars
+ ${un.SetBrandNameVars} "$INSTDIR\distribution\setup.ini"
+ !endif
+
+ ; Initialize $hHeaderBitmap to prevent redundant changing of the bitmap if
+ ; the user clicks the back button
+ StrCpy $hHeaderBitmap ""
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro un.UninstallUnOnInitCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.UninstallUnOnInitCommon
+ !verbose pop
+!macroend
+
+/**
+ * Called from the MUI leaveOptions function to set the value of $INSTDIR.
+ */
+!macro LeaveOptionsCommon
+
+ !ifndef LeaveOptionsCommon
+ !insertmacro CanWriteToInstallDir
+ !insertmacro GetLongPath
+
+!ifndef NO_INSTDIR_FROM_REG
+ !insertmacro GetSingleInstallPath
+!endif
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LeaveOptionsCommon "!insertmacro LeaveOptionsCommonCall"
+
+ Function LeaveOptionsCommon
+ Push $R9
+
+!ifndef NO_INSTDIR_FROM_REG
+ SetShellVarContext all ; Set SHCTX to HKLM
+ ${GetSingleInstallPath} "Software\Mozilla\${BrandFullNameInternal}" $R9
+
+ StrCmp "$R9" "false" +1 finish_get_install_dir
+
+ SetShellVarContext current ; Set SHCTX to HKCU
+ ${GetSingleInstallPath} "Software\Mozilla\${BrandFullNameInternal}" $R9
+
+ ${If} ${RunningX64}
+ ; In HKCU there is no WOW64 redirection, which means we may have gotten
+ ; the path to a 32-bit install even though we're 64-bit, or vice-versa.
+ ; In that case, just use the default path instead of offering an upgrade.
+ ; But only do that override if the existing install is in Program Files,
+ ; because that's the only place we can be sure is specific
+ ; to either 32 or 64 bit applications.
+ ; The WordFind syntax below searches for the first occurence of the
+ ; "delimiter" (the Program Files path) in the install path and returns
+ ; anything that appears before that. If nothing appears before that,
+ ; then the install is under Program Files (32 or 64).
+!ifdef HAVE_64BIT_BUILD
+ ${WordFind} $R9 $PROGRAMFILES32 "+1{" $0
+!else
+ ${WordFind} $R9 $PROGRAMFILES64 "+1{" $0
+!endif
+ ${If} $0 == ""
+ StrCpy $R9 "false"
+ ${EndIf}
+ ${EndIf}
+
+ finish_get_install_dir:
+ StrCmp "$R9" "false" +2 +1
+ StrCpy $INSTDIR "$R9"
+!endif
+
+ ; If the user doesn't have write access to the installation directory set
+ ; the installation directory to a subdirectory of the All Users application
+ ; directory and if the user can't write to that location set the installation
+ ; directory to a subdirectory of the users local application directory
+ ; (e.g. non-roaming).
+ ${CanWriteToInstallDir} $R9
+ StrCmp "$R9" "false" +1 finish_check_install_dir
+
+ SetShellVarContext all ; Set SHCTX to All Users
+ StrCpy $INSTDIR "$APPDATA\${BrandFullName}\"
+ ${CanWriteToInstallDir} $R9
+ StrCmp "$R9" "false" +2 +1
+ StrCpy $INSTDIR "$LOCALAPPDATA\${BrandFullName}\"
+
+ finish_check_install_dir:
+ IfFileExists "$INSTDIR" +3 +1
+ Pop $R9
+ Return
+
+ ; Always display the long path if the path already exists.
+ ${GetLongPath} "$INSTDIR" $INSTDIR
+
+ ; The call to GetLongPath returns a long path without a trailing
+ ; back-slash. Append a \ to the path to prevent the directory
+ ; name from being appended when using the NSIS create new folder.
+ ; http://www.nullsoft.com/free/nsis/makensis.htm#InstallDir
+ StrCpy $INSTDIR "$INSTDIR\"
+
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LeaveOptionsCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call LeaveOptionsCommon
+ !verbose pop
+!macroend
+
+/**
+ * Called from the MUI preDirectory function to verify there is enough disk
+ * space for the installation and the installation directory is writable.
+ *
+ * $R9 = returned value from CheckDiskSpace and CanWriteToInstallDir macros
+ */
+!macro PreDirectoryCommon
+
+ !ifndef PreDirectoryCommon
+ !insertmacro CanWriteToInstallDir
+ !insertmacro CheckDiskSpace
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define PreDirectoryCommon "!insertmacro PreDirectoryCommonCall"
+
+ Function PreDirectoryCommon
+ Push $R9
+
+ IntCmp $InstallType ${INSTALLTYPE_CUSTOM} end +1 +1
+ ${CanWriteToInstallDir} $R9
+ StrCmp "$R9" "false" end +1
+ ${CheckDiskSpace} $R9
+ StrCmp "$R9" "false" end +1
+ Abort
+
+ end:
+
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro PreDirectoryCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call PreDirectoryCommon
+ !verbose pop
+!macroend
+
+/**
+ * Called from the MUI leaveDirectory function
+ *
+ * @param _WARN_DISK_SPACE
+ * Message displayed when there isn't enough disk space to perform the
+ * installation.
+ * @param _WARN_WRITE_ACCESS
+ * Message displayed when the installer does not have write access to
+ * $INSTDIR.
+ *
+ * $R7 = returned value from CheckDiskSpace and CanWriteToInstallDir macros
+ * $R8 = _WARN_DISK_SPACE
+ * $R9 = _WARN_WRITE_ACCESS
+ */
+!macro LeaveDirectoryCommon
+
+ !ifndef LeaveDirectoryCommon
+ !insertmacro CheckDiskSpace
+ !insertmacro CanWriteToInstallDir
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LeaveDirectoryCommon "!insertmacro LeaveDirectoryCommonCall"
+
+ Function LeaveDirectoryCommon
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Push $R7
+
+ ${CanWriteToInstallDir} $R7
+ ${If} $R7 == "false"
+ MessageBox MB_OK|MB_ICONEXCLAMATION "$R9"
+ Abort
+ ${EndIf}
+
+ ${CheckDiskSpace} $R7
+ ${If} $R7 == "false"
+ MessageBox MB_OK|MB_ICONEXCLAMATION "$R8"
+ Abort
+ ${EndIf}
+
+ Pop $R7
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LeaveDirectoryCommonCall _WARN_DISK_SPACE _WARN_WRITE_ACCESS
+ !verbose push
+ Push "${_WARN_DISK_SPACE}"
+ Push "${_WARN_WRITE_ACCESS}"
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call LeaveDirectoryCommon
+ !verbose pop
+!macroend
+
+
+################################################################################
+# Install Section common macros.
+
+/**
+ * Performs common cleanup operations prior to the actual installation.
+ * This macro should be called first when installation starts.
+ */
+!macro InstallStartCleanupCommon
+
+ !ifndef InstallStartCleanupCommon
+ !insertmacro CleanVirtualStore
+ !insertmacro EndUninstallLog
+ !insertmacro OnInstallUninstall
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define InstallStartCleanupCommon "!insertmacro InstallStartCleanupCommonCall"
+
+ Function InstallStartCleanupCommon
+
+ ; Remove files not removed by parsing the uninstall.log
+ Delete "$INSTDIR\install_wizard.log"
+ Delete "$INSTDIR\install_status.log"
+
+ RmDir /r "$INSTDIR\updates"
+ Delete "$INSTDIR\updates.xml"
+ Delete "$INSTDIR\active-update.xml"
+
+ ; Remove files from the uninstall directory.
+ ${If} ${FileExists} "$INSTDIR\uninstall"
+ Delete "$INSTDIR\uninstall\*wizard*"
+ Delete "$INSTDIR\uninstall\uninstall.ini"
+ Delete "$INSTDIR\uninstall\cleanup.log"
+ Delete "$INSTDIR\uninstall\uninstall.update"
+ ${OnInstallUninstall}
+ ${EndIf}
+
+ ; Since we write to the uninstall.log in this directory during the
+ ; installation create the directory if it doesn't already exist.
+ IfFileExists "$INSTDIR\uninstall" +2 +1
+ CreateDirectory "$INSTDIR\uninstall"
+
+ ; Application update uses a directory named tobedeleted in the $INSTDIR to
+ ; delete files on OS reboot when they are in use. Try to delete this
+ ; directory if it exists.
+ ${If} ${FileExists} "$INSTDIR\${TO_BE_DELETED}"
+ RmDir /r "$INSTDIR\${TO_BE_DELETED}"
+ ${EndIf}
+
+ ; Remove files that may be left behind by the application in the
+ ; VirtualStore directory.
+ ${CleanVirtualStore}
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro InstallStartCleanupCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call InstallStartCleanupCommon
+ !verbose pop
+!macroend
+
+/**
+ * Performs common cleanup operations after the actual installation.
+ * This macro should be called last during the installation.
+ */
+!macro InstallEndCleanupCommon
+
+ !ifndef InstallEndCleanupCommon
+ !insertmacro EndUninstallLog
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define InstallEndCleanupCommon "!insertmacro InstallEndCleanupCommonCall"
+
+ Function InstallEndCleanupCommon
+
+ ; Close the file handle to the uninstall.log
+ ${EndUninstallLog}
+
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro InstallEndCleanupCommonCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call InstallEndCleanupCommon
+ !verbose pop
+!macroend
+
+
+################################################################################
+# UAC Related Macros
+
+/**
+ * Provides UAC elevation support for Vista and above (requires the UAC plugin).
+ *
+ * $0 = return values from calls to the UAC plugin (always uses $0)
+ * $R9 = return values from GetParameters and GetOptions macros
+ */
+!macro ElevateUAC
+
+ !ifndef ${_MOZFUNC_UN}ElevateUAC
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetOptions
+ !insertmacro ${_MOZFUNC_UN_TMP}GetParameters
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}ElevateUAC "!insertmacro ${_MOZFUNC_UN}ElevateUACCall"
+
+ Function ${_MOZFUNC_UN}ElevateUAC
+ Push $R9
+ Push $0
+
+!ifndef NONADMIN_ELEVATE
+ ${If} ${AtLeastWinVista}
+ UAC::IsAdmin
+ ; If the user is not an admin already
+ ${If} "$0" != "1"
+ UAC::SupportsUAC
+ ; If the system supports UAC
+ ${If} "$0" == "1"
+ UAC::GetElevationType
+ ; If the user account has a split token
+ ${If} "$0" == "3"
+ UAC::RunElevated
+ UAC::Unload
+ ; Nothing besides UAC initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ${EndIf}
+ ${Else}
+ ${GetParameters} $R9
+ ${If} $R9 != ""
+ ClearErrors
+ ${GetOptions} "$R9" "/UAC:" $0
+ ; If the command line contains /UAC then we need to initialize
+ ; the UAC plugin to use UAC::ExecCodeSegment to execute code in
+ ; the non-elevated context.
+ ${Unless} ${Errors}
+ UAC::RunElevated
+ ${EndUnless}
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+!else
+ ${If} ${AtLeastWinVista}
+ UAC::IsAdmin
+ ; If the user is not an admin already
+ ${If} "$0" != "1"
+ UAC::SupportsUAC
+ ; If the system supports UAC require that the user elevate
+ ${If} "$0" == "1"
+ UAC::GetElevationType
+ ; If the user account has a split token
+ ${If} "$0" == "3"
+ UAC::RunElevated
+ UAC::Unload
+ ; Nothing besides UAC initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ${Else}
+ ; Check if UAC is enabled. If the user has turned UAC on or off
+ ; without rebooting this value will be incorrect. This is an
+ ; edgecase that we have to live with when trying to allow
+ ; installing when the user doesn't have privileges such as a public
+ ; computer while trying to also achieve UAC elevation. When this
+ ; happens the user will be presented with the runas dialog if the
+ ; value is 1 and won't be presented with the UAC dialog when the
+ ; value is 0.
+ ReadRegDWord $R9 HKLM "SOFTWARE\Microsoft\Windows\CurrentVersion\Policies\System" "EnableLUA"
+ ${If} "$R9" == "1"
+ ; This will display the UAC version of the runas dialog which
+ ; requires a password for an existing user account.
+ UAC::RunElevated
+ ${If} "$0" == "0" ; Was elevation successful
+ UAC::Unload
+ ; Nothing besides UAC initialized so no need to call OnEndCommon
+ Quit
+ ${EndIf}
+ ; Unload UAC since the elevation request was not successful and
+ ; install anyway.
+ UAC::Unload
+ ${EndIf}
+ ${EndIf}
+ ${Else}
+ ClearErrors
+ ${${_MOZFUNC_UN}GetParameters} $R9
+ ${${_MOZFUNC_UN}GetOptions} "$R9" "/UAC:" $R9
+ ; If the command line contains /UAC then we need to initialize the UAC
+ ; plugin to use UAC::ExecCodeSegment to execute code in the
+ ; non-elevated context.
+ ${Unless} ${Errors}
+ UAC::RunElevated
+ ${EndUnless}
+ ${EndIf}
+ ${EndIf}
+!endif
+
+ ClearErrors
+
+ Pop $0
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro ElevateUACCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call ElevateUAC
+ !verbose pop
+!macroend
+
+!macro un.ElevateUACCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.ElevateUAC
+ !verbose pop
+!macroend
+
+!macro un.ElevateUAC
+ !ifndef un.ElevateUAC
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro ElevateUAC
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Unloads the UAC plugin so the NSIS plugins can be removed when the installer
+ * and uninstaller exit.
+ *
+ * $R9 = return values from GetParameters and GetOptions macros
+ */
+!macro UnloadUAC
+
+ !ifndef ${_MOZFUNC_UN}UnloadUAC
+ !define _MOZFUNC_UN_TMP_UnloadUAC ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP_UnloadUAC}GetOptions
+ !insertmacro ${_MOZFUNC_UN_TMP_UnloadUAC}GetParameters
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP_UnloadUAC}
+ !undef _MOZFUNC_UN_TMP_UnloadUAC
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}UnloadUAC "!insertmacro ${_MOZFUNC_UN}UnloadUACCall"
+
+ Function ${_MOZFUNC_UN}UnloadUAC
+ ${Unless} ${AtLeastWinVista}
+ Return
+ ${EndUnless}
+
+ Push $R9
+
+ ClearErrors
+ ${${_MOZFUNC_UN}GetParameters} $R9
+ ${${_MOZFUNC_UN}GetOptions} "$R9" "/UAC:" $R9
+ ; If the command line contains /UAC then we need to unload the UAC plugin
+ IfErrors +2 +1
+ UAC::Unload
+
+ ClearErrors
+
+ Pop $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro UnloadUACCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call UnloadUAC
+ !verbose pop
+!macroend
+
+!macro un.UnloadUACCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.UnloadUAC
+ !verbose pop
+!macroend
+
+!macro un.UnloadUAC
+ !ifndef un.UnloadUAC
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro UnloadUAC
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+
+################################################################################
+# Macros for uninstall.log and install.log logging
+#
+# Since these are used by other macros they should be inserted first. All of
+# these macros can be easily inserted using the _LoggingCommon macro.
+
+/**
+ * Adds all logging macros in the correct order in one fell swoop as well as
+ * the vars for the install.log and uninstall.log file handles.
+ */
+!macro _LoggingCommon
+ Var /GLOBAL fhInstallLog
+ Var /GLOBAL fhUninstallLog
+ !insertmacro StartInstallLog
+ !insertmacro EndInstallLog
+ !insertmacro StartUninstallLog
+ !insertmacro EndUninstallLog
+!macroend
+!define _LoggingCommon "!insertmacro _LoggingCommon"
+
+/**
+ * Creates a file named install.log in the install directory (e.g. $INSTDIR)
+ * and adds the installation started message to the install.log for this
+ * installation. This also adds the fhInstallLog and fhUninstallLog vars used
+ * for logging.
+ *
+ * $fhInstallLog = filehandle for $INSTDIR\install.log
+ *
+ * @param _APP_NAME
+ * Typically the BrandFullName
+ * @param _AB_CD
+ * The locale identifier
+ * @param _APP_VERSION
+ * The application version
+ * @param _GRE_VERSION
+ * The Gecko Runtime Engine version
+ *
+ * $R6 = _APP_NAME
+ * $R7 = _AB_CD
+ * $R8 = _APP_VERSION
+ * $R9 = _GRE_VERSION
+ */
+!macro StartInstallLog
+
+ !ifndef StartInstallLog
+ !insertmacro GetTime
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define StartInstallLog "!insertmacro StartInstallLogCall"
+
+ Function StartInstallLog
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Exch 2
+ Exch $R7
+ Exch 3
+ Exch $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+ Push $R1
+ Push $R0
+ Push $9
+
+ ${DeleteFile} "$INSTDIR\install.log"
+ FileOpen $fhInstallLog "$INSTDIR\install.log" w
+ FileWriteWord $fhInstallLog "65279"
+
+ ${GetTime} "" "L" $9 $R0 $R1 $R2 $R3 $R4 $R5
+ FileWriteUTF16LE $fhInstallLog "$R6 Installation Started: $R1-$R0-$9 $R3:$R4:$R5"
+ ${WriteLogSeparator}
+
+ ${LogHeader} "Installation Details"
+ ${LogMsg} "Install Dir: $INSTDIR"
+ ${LogMsg} "Locale : $R7"
+ ${LogMsg} "App Version: $R8"
+ ${LogMsg} "GRE Version: $R9"
+
+ ${If} ${IsWinXP}
+ ${LogMsg} "OS Name : Windows XP"
+ ${ElseIf} ${IsWin2003}
+ ${LogMsg} "OS Name : Windows 2003"
+ ${ElseIf} ${IsWinVista}
+ ${LogMsg} "OS Name : Windows Vista"
+ ${ElseIf} ${IsWin7}
+ ${LogMsg} "OS Name : Windows 7"
+ ${ElseIf} ${IsWin8}
+ ${LogMsg} "OS Name : Windows 8"
+ ${ElseIf} ${IsWin8.1}
+ ${LogMsg} "OS Name : Windows 8.1"
+ ${ElseIf} ${IsWin10}
+ ${LogMsg} "OS Name : Windows 10"
+ ${ElseIf} ${AtLeastWin10}
+ ${LogMsg} "OS Name : Above Windows 10"
+ ${Else}
+ ${LogMsg} "OS Name : Unable to detect"
+ ${EndIf}
+
+ !ifdef HAVE_64BIT_BUILD
+ ${LogMsg} "Target CPU : x64"
+ !else
+ ${LogMsg} "Target CPU : x86"
+ !endif
+
+ Pop $9
+ Pop $R0
+ Pop $R1
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Exch $R6
+ Exch 3
+ Exch $R7
+ Exch 2
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro StartInstallLogCall _APP_NAME _AB_CD _APP_VERSION _GRE_VERSION
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_APP_NAME}"
+ Push "${_AB_CD}"
+ Push "${_APP_VERSION}"
+ Push "${_GRE_VERSION}"
+ Call StartInstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Writes the installation finished message to the install.log and closes the
+ * file handles to the install.log and uninstall.log
+ *
+ * @param _APP_NAME
+ *
+ * $R9 = _APP_NAME
+ */
+!macro EndInstallLog
+
+ !ifndef EndInstallLog
+ !insertmacro GetTime
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define EndInstallLog "!insertmacro EndInstallLogCall"
+
+ Function EndInstallLog
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3
+ Push $R2
+
+ ${WriteLogSeparator}
+ ${GetTime} "" "L" $R2 $R3 $R4 $R5 $R6 $R7 $R8
+ FileWriteUTF16LE $fhInstallLog "$R9 Installation Finished: $R4-$R3-$R2 $R6:$R7:$R8$\r$\n"
+ FileClose $fhInstallLog
+
+ Pop $R2
+ Pop $R3
+ Pop $R4
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro EndInstallLogCall _APP_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_APP_NAME}"
+ Call EndInstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Opens the file handle to the uninstall.log.
+ *
+ * $fhUninstallLog = filehandle for $INSTDIR\uninstall\uninstall.log
+ */
+!macro StartUninstallLog
+
+ !ifndef StartUninstallLog
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define StartUninstallLog "!insertmacro StartUninstallLogCall"
+
+ Function StartUninstallLog
+ FileOpen $fhUninstallLog "$INSTDIR\uninstall\uninstall.log" w
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro StartUninstallLogCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call StartUninstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Closes the file handle to the uninstall.log.
+ */
+!macro EndUninstallLog
+
+ !ifndef EndUninstallLog
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define EndUninstallLog "!insertmacro EndUninstallLogCall"
+
+ Function EndUninstallLog
+ FileClose $fhUninstallLog
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro EndUninstallLogCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call EndUninstallLog
+ !verbose pop
+!macroend
+
+/**
+ * Adds a section header to the human readable log.
+ *
+ * @param _HEADER
+ * The header text to write to the log.
+ */
+!macro LogHeader _HEADER
+ ${WriteLogSeparator}
+ FileWriteUTF16LE $fhInstallLog "${_HEADER}"
+ ${WriteLogSeparator}
+!macroend
+!define LogHeader "!insertmacro LogHeader"
+
+/**
+ * Adds a section message to the human readable log.
+ *
+ * @param _MSG
+ * The message text to write to the log.
+ */
+!macro LogMsg _MSG
+ FileWriteUTF16LE $fhInstallLog " ${_MSG}$\r$\n"
+!macroend
+!define LogMsg "!insertmacro LogMsg"
+
+/**
+ * Adds an uninstall entry to the uninstall log.
+ *
+ * @param _MSG
+ * The message text to write to the log.
+ */
+!macro LogUninstall _MSG
+ FileWrite $fhUninstallLog "${_MSG}$\r$\n"
+!macroend
+!define LogUninstall "!insertmacro LogUninstall"
+
+/**
+ * Adds a section divider to the human readable log.
+ */
+!macro WriteLogSeparator
+ FileWriteUTF16LE $fhInstallLog "$\r$\n----------------------------------------\
+ ---------------------------------------$\r$\n"
+!macroend
+!define WriteLogSeparator "!insertmacro WriteLogSeparator"
+
+
+################################################################################
+# Macros for managing the shortcuts log ini file
+
+/**
+ * Adds the most commonly used shortcut logging macros for the installer in one
+ * fell swoop.
+ */
+!macro _LoggingShortcutsCommon
+ !insertmacro LogDesktopShortcut
+ !insertmacro LogQuickLaunchShortcut
+ !insertmacro LogSMProgramsShortcut
+!macroend
+!define _LoggingShortcutsCommon "!insertmacro _LoggingShortcutsCommon"
+
+/**
+ * Creates the shortcuts log ini file with a UTF-16LE BOM if it doesn't exist.
+ */
+!macro initShortcutsLog
+ Push $R9
+
+ IfFileExists "$INSTDIR\uninstall\${SHORTCUTS_LOG}" +4 +1
+ FileOpen $R9 "$INSTDIR\uninstall\${SHORTCUTS_LOG}" w
+ FileWriteWord $R9 "65279"
+ FileClose $R9
+
+ Pop $R9
+!macroend
+!define initShortcutsLog "!insertmacro initShortcutsLog"
+
+/**
+ * Adds shortcut entries to the shortcuts log ini file. This macro is primarily
+ * a helper used by the LogDesktopShortcut, LogQuickLaunchShortcut, and
+ * LogSMProgramsShortcut macros but it can be used by other code if desired. If
+ * the value already exists the the value is not written to the file.
+ *
+ * @param _SECTION_NAME
+ * The section name to write to in the shortcut log ini file
+ * @param _FILE_NAME
+ * The shortcut's file name
+ *
+ * $R6 = return value from ReadIniStr for the shortcut file name
+ * $R7 = counter for supporting multiple shortcuts in the same location
+ * $R8 = _SECTION_NAME
+ * $R9 = _FILE_NAME
+ */
+!macro LogShortcut
+
+ !ifndef LogShortcut
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LogShortcut "!insertmacro LogShortcutCall"
+
+ Function LogShortcut
+ Exch $R9
+ Exch 1
+ Exch $R8
+ Push $R7
+ Push $R6
+
+ ClearErrors
+
+ !insertmacro initShortcutsLog
+
+ StrCpy $R6 ""
+ StrCpy $R7 -1
+
+ StrCmp "$R6" "$R9" +5 +1 ; if the shortcut already exists don't add it
+ IntOp $R7 $R7 + 1 ; increment the counter
+ ReadIniStr $R6 "$INSTDIR\uninstall\${SHORTCUTS_LOG}" "$R8" "Shortcut$R7"
+ IfErrors +1 -3
+ WriteINIStr "$INSTDIR\uninstall\${SHORTCUTS_LOG}" "$R8" "Shortcut$R7" "$R9"
+
+ ClearErrors
+
+ Pop $R6
+ Pop $R7
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LogShortcutCall _SECTION_NAME _FILE_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_SECTION_NAME}"
+ Push "${_FILE_NAME}"
+ Call LogShortcut
+ !verbose pop
+!macroend
+
+/**
+ * Adds a Desktop shortcut entry to the shortcuts log ini file.
+ *
+ * @param _FILE_NAME
+ * The shortcut file name (e.g. shortcut.lnk)
+ */
+!macro LogDesktopShortcut
+
+ !ifndef LogDesktopShortcut
+ !insertmacro LogShortcut
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LogDesktopShortcut "!insertmacro LogDesktopShortcutCall"
+
+ Function LogDesktopShortcut
+ Call LogShortcut
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LogDesktopShortcutCall _FILE_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "DESKTOP"
+ Push "${_FILE_NAME}"
+ Call LogDesktopShortcut
+ !verbose pop
+!macroend
+
+/**
+ * Adds a QuickLaunch shortcut entry to the shortcuts log ini file.
+ *
+ * @param _FILE_NAME
+ * The shortcut file name (e.g. shortcut.lnk)
+ */
+!macro LogQuickLaunchShortcut
+
+ !ifndef LogQuickLaunchShortcut
+ !insertmacro LogShortcut
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LogQuickLaunchShortcut "!insertmacro LogQuickLaunchShortcutCall"
+
+ Function LogQuickLaunchShortcut
+ Call LogShortcut
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LogQuickLaunchShortcutCall _FILE_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "QUICKLAUNCH"
+ Push "${_FILE_NAME}"
+ Call LogQuickLaunchShortcut
+ !verbose pop
+!macroend
+
+/**
+ * Adds a Start Menu shortcut entry to the shortcuts log ini file.
+ *
+ * @param _FILE_NAME
+ * The shortcut file name (e.g. shortcut.lnk)
+ */
+!macro LogStartMenuShortcut
+
+ !ifndef LogStartMenuShortcut
+ !insertmacro LogShortcut
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LogStartMenuShortcut "!insertmacro LogStartMenuShortcutCall"
+
+ Function LogStartMenuShortcut
+ Call LogShortcut
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LogStartMenuShortcutCall _FILE_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "STARTMENU"
+ Push "${_FILE_NAME}"
+ Call LogStartMenuShortcut
+ !verbose pop
+!macroend
+
+/**
+ * Adds a Start Menu Programs shortcut entry to the shortcuts log ini file.
+ *
+ * @param _FILE_NAME
+ * The shortcut file name (e.g. shortcut.lnk)
+ */
+!macro LogSMProgramsShortcut
+
+ !ifndef LogSMProgramsShortcut
+ !insertmacro LogShortcut
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define LogSMProgramsShortcut "!insertmacro LogSMProgramsShortcutCall"
+
+ Function LogSMProgramsShortcut
+ Call LogShortcut
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro LogSMProgramsShortcutCall _FILE_NAME
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "SMPROGRAMS"
+ Push "${_FILE_NAME}"
+ Call LogSMProgramsShortcut
+ !verbose pop
+!macroend
+
+/**
+ * Adds the relative path from the Start Menu Programs directory for the
+ * application's Start Menu directory if it is different from the existing value
+ * to the shortcuts log ini file.
+ *
+ * @param _REL_PATH_TO_DIR
+ * The relative path from the Start Menu Programs directory to the
+ * program's directory.
+ *
+ * $R9 = _REL_PATH_TO_DIR
+ */
+!macro LogSMProgramsDirRelPath _REL_PATH_TO_DIR
+ Push $R9
+
+ !insertmacro initShortcutsLog
+
+ ReadINIStr $R9 "$INSTDIR\uninstall\${SHORTCUTS_LOG}" "SMPROGRAMS" "RelativePathToDir"
+ StrCmp "$R9" "${_REL_PATH_TO_DIR}" +2 +1
+ WriteINIStr "$INSTDIR\uninstall\${SHORTCUTS_LOG}" "SMPROGRAMS" "RelativePathToDir" "${_REL_PATH_TO_DIR}"
+
+ Pop $R9
+!macroend
+!define LogSMProgramsDirRelPath "!insertmacro LogSMProgramsDirRelPath"
+
+/**
+ * Copies the value for the relative path from the Start Menu programs directory
+ * (e.g. $SMPROGRAMS) to the Start Menu directory as it is stored in the
+ * shortcuts log ini file to the variable specified in the first parameter.
+ */
+!macro GetSMProgramsDirRelPath _VAR
+ ReadINIStr ${_VAR} "$INSTDIR\uninstall\${SHORTCUTS_LOG}" "SMPROGRAMS" \
+ "RelativePathToDir"
+!macroend
+!define GetSMProgramsDirRelPath "!insertmacro GetSMProgramsDirRelPath"
+
+/**
+ * Copies the shortcuts log ini file path to the variable specified in the
+ * first parameter.
+ */
+!macro GetShortcutsLogPath _VAR
+ StrCpy ${_VAR} "$INSTDIR\uninstall\${SHORTCUTS_LOG}"
+!macroend
+!define GetShortcutsLogPath "!insertmacro GetShortcutsLogPath"
+
+/**
+ * Deletes the shortcuts log ini file.
+ */
+!macro DeleteShortcutsLogFile
+ ${DeleteFile} "$INSTDIR\uninstall\${SHORTCUTS_LOG}"
+!macroend
+!define DeleteShortcutsLogFile "!insertmacro DeleteShortcutsLogFile"
+
+
+################################################################################
+# Macros for managing specific Windows version features
+
+/**
+ * Sets the permitted layered service provider (LSP) categories on Windows
+ * Vista and above for the application. Consumers should call this after an
+ * installation log section has completed since this macro will log the results
+ * to the installation log along with a header.
+ *
+ * !IMPORTANT - When calling this macro from an uninstaller do not specify a
+ * parameter. The paramter is hardcoded with 0x00000000 to remove
+ * the LSP category for the application when performing an
+ * uninstall.
+ *
+ * @param _LSP_CATEGORIES
+ * The permitted LSP categories for the application. When called by an
+ * uninstaller this will always be 0x00000000.
+ *
+ * $R5 = error code popped from the stack for the WSCSetApplicationCategory call
+ * $R6 = return value from the WSCSetApplicationCategory call
+ * $R7 = string length for the long path to the main application executable
+ * $R8 = long path to the main application executable
+ * $R9 = _LSP_CATEGORIES
+ */
+!macro SetAppLSPCategories
+
+ !ifndef ${_MOZFUNC_UN}SetAppLSPCategories
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}SetAppLSPCategories "!insertmacro ${_MOZFUNC_UN}SetAppLSPCategoriesCall"
+
+ Function ${_MOZFUNC_UN}SetAppLSPCategories
+ ${Unless} ${AtLeastWinVista}
+ Return
+ ${EndUnless}
+
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR\${FileMainEXE}" $R8
+ StrLen $R7 "$R8"
+
+ ; Remove existing categories by setting the permitted categories to
+ ; 0x00000000 since new categories are ANDed with existing categories. If
+ ; the param value stored in $R9 is 0x00000000 then skip the removal since
+ ; the categories will be removed by the second call to
+ ; WSCSetApplicationCategory.
+ StrCmp "$R9" "0x00000000" +2 +1
+ System::Call "Ws2_32::WSCSetApplicationCategory(w R8, i R7, w n, i 0,\
+ i 0x00000000, i n, *i) i"
+
+ ; Set the permitted LSP categories
+ System::Call "Ws2_32::WSCSetApplicationCategory(w R8, i R7, w n, i 0,\
+ i R9, i n, *i .s) i.R6"
+ Pop $R5
+
+!ifndef NO_LOG
+ ${LogHeader} "Setting Permitted LSP Categories"
+ StrCmp "$R6" 0 +3 +1
+ ${LogMsg} "** ERROR Setting LSP Categories: $R5 **"
+ GoTo +2
+ ${LogMsg} "Permitted LSP Categories: $R9"
+!endif
+
+ ClearErrors
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro SetAppLSPCategoriesCall _LSP_CATEGORIES
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_LSP_CATEGORIES}"
+ Call SetAppLSPCategories
+ !verbose pop
+!macroend
+
+!macro un.SetAppLSPCategoriesCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "0x00000000"
+ Call un.SetAppLSPCategories
+ !verbose pop
+!macroend
+
+!macro un.SetAppLSPCategories
+ !ifndef un.SetAppLSPCategories
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro SetAppLSPCategories
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Checks if any pinned TaskBar lnk files point to the executable's path passed
+ * to the macro.
+ *
+ * @param _EXE_PATH
+ * The executable path
+ * @return _RESULT
+ * false if no pinned shotcuts were found for this install location.
+ * true if pinned shotcuts were found for this install location.
+ *
+ * $R5 = stores whether a TaskBar lnk file has been found for the executable
+ * $R6 = long path returned from GetShortCutTarget and GetLongPath
+ * $R7 = file name returned from FindFirst and FindNext
+ * $R8 = find handle for FindFirst and FindNext
+ * $R9 = _EXE_PATH and _RESULT
+ */
+!macro IsPinnedToTaskBar
+
+ !ifndef IsPinnedToTaskBar
+ !insertmacro GetLongPath
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define IsPinnedToTaskBar "!insertmacro IsPinnedToTaskBarCall"
+
+ Function IsPinnedToTaskBar
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ StrCpy $R5 "false"
+
+ ${If} ${AtLeastWin7}
+ ${AndIf} ${FileExists} "$QUICKLAUNCH\User Pinned\TaskBar"
+ FindFirst $R8 $R7 "$QUICKLAUNCH\User Pinned\TaskBar\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$QUICKLAUNCH\User Pinned\TaskBar\$R7"
+ ShellLink::GetShortCutTarget "$QUICKLAUNCH\User Pinned\TaskBar\$R7"
+ Pop $R6
+ ${GetLongPath} "$R6" $R6
+ ${If} "$R6" == "$R9"
+ StrCpy $R5 "true"
+ ${ExitDo}
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R8 $R7
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R8
+ ${EndIf}
+
+ ClearErrors
+
+ StrCpy $R9 $R5
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro IsPinnedToTaskBarCall _EXE_PATH _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_EXE_PATH}"
+ Call IsPinnedToTaskBar
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+/**
+ * Checks if any pinned Start Menu lnk files point to the executable's path
+ * passed to the macro.
+ *
+ * @param _EXE_PATH
+ * The executable path
+ * @return _RESULT
+ * false if no pinned shotcuts were found for this install location.
+ * true if pinned shotcuts were found for this install location.
+ *
+ * $R5 = stores whether a Start Menu lnk file has been found for the executable
+ * $R6 = long path returned from GetShortCutTarget and GetLongPath
+ * $R7 = file name returned from FindFirst and FindNext
+ * $R8 = find handle for FindFirst and FindNext
+ * $R9 = _EXE_PATH and _RESULT
+ */
+!macro IsPinnedToStartMenu
+
+ !ifndef IsPinnedToStartMenu
+ !insertmacro GetLongPath
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define IsPinnedToStartMenu "!insertmacro IsPinnedToStartMenuCall"
+
+ Function IsPinnedToStartMenu
+ Exch $R9
+ Push $R8
+ Push $R7
+ Push $R6
+ Push $R5
+
+ StrCpy $R5 "false"
+
+ ${If} ${AtLeastWin7}
+ ${AndIf} ${FileExists} "$QUICKLAUNCH\User Pinned\StartMenu"
+ FindFirst $R8 $R7 "$QUICKLAUNCH\User Pinned\StartMenu\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$QUICKLAUNCH\User Pinned\StartMenu\$R7"
+ ShellLink::GetShortCutTarget "$QUICKLAUNCH\User Pinned\StartMenu\$R7"
+ Pop $R6
+ ${GetLongPath} "$R6" $R6
+ ${If} "$R6" == "$R9"
+ StrCpy $R5 "true"
+ ${ExitDo}
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R8 $R7
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R8
+ ${EndIf}
+
+ ClearErrors
+
+ StrCpy $R9 $R5
+
+ Pop $R5
+ Pop $R6
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro IsPinnedToStartMenuCall _EXE_PATH _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_EXE_PATH}"
+ Call IsPinnedToStartMenu
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+/**
+ * Gets the number of pinned shortcut lnk files pinned to the Task Bar.
+ *
+ * @return _RESULT
+ * number of pinned shortcut lnk files.
+ *
+ * $R7 = file name returned from FindFirst and FindNext
+ * $R8 = find handle for FindFirst and FindNext
+ * $R9 = _RESULT
+ */
+!macro PinnedToTaskBarLnkCount
+
+ !ifndef PinnedToTaskBarLnkCount
+ !insertmacro GetLongPath
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define PinnedToTaskBarLnkCount "!insertmacro PinnedToTaskBarLnkCountCall"
+
+ Function PinnedToTaskBarLnkCount
+ Push $R9
+ Push $R8
+ Push $R7
+
+ StrCpy $R9 0
+
+ ${If} ${AtLeastWin7}
+ ${AndIf} ${FileExists} "$QUICKLAUNCH\User Pinned\TaskBar"
+ FindFirst $R8 $R7 "$QUICKLAUNCH\User Pinned\TaskBar\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$QUICKLAUNCH\User Pinned\TaskBar\$R7"
+ IntOp $R9 $R9 + 1
+ ${EndIf}
+ ClearErrors
+ FindNext $R8 $R7
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R8
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro PinnedToTaskBarLnkCountCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call PinnedToTaskBarLnkCount
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+/**
+ * Gets the number of pinned shortcut lnk files pinned to the Start Menu.
+ *
+ * @return _RESULT
+ * number of pinned shortcut lnk files.
+ *
+ * $R7 = file name returned from FindFirst and FindNext
+ * $R8 = find handle for FindFirst and FindNext
+ * $R9 = _RESULT
+ */
+!macro PinnedToStartMenuLnkCount
+
+ !ifndef PinnedToStartMenuLnkCount
+ !insertmacro GetLongPath
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define PinnedToStartMenuLnkCount "!insertmacro PinnedToStartMenuLnkCountCall"
+
+ Function PinnedToStartMenuLnkCount
+ Push $R9
+ Push $R8
+ Push $R7
+
+ StrCpy $R9 0
+
+ ${If} ${AtLeastWin7}
+ ${AndIf} ${FileExists} "$QUICKLAUNCH\User Pinned\StartMenu"
+ FindFirst $R8 $R7 "$QUICKLAUNCH\User Pinned\StartMenu\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$QUICKLAUNCH\User Pinned\StartMenu\$R7"
+ IntOp $R9 $R9 + 1
+ ${EndIf}
+ ClearErrors
+ FindNext $R8 $R7
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R8
+ ${EndIf}
+
+ ClearErrors
+
+ Pop $R7
+ Pop $R8
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro PinnedToStartMenuLnkCountCall _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call PinnedToStartMenuLnkCount
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+/**
+ * Update Start Menu / TaskBar lnk files that point to the executable's path
+ * passed to the macro and all other shortcuts installed by the application with
+ * the current application user model ID. Requires ApplicationID.
+ *
+ * NOTE: this does not update Desktop shortcut application user model ID due to
+ * bug 633728.
+ *
+ * @param _EXE_PATH
+ * The main application executable path
+ * @param _APP_ID
+ * The application user model ID for the current install
+ * @return _RESULT
+ * false if no pinned shotcuts were found for this install location.
+ * true if pinned shotcuts were found for this install location.
+ */
+!macro UpdateShortcutAppModelIDs
+
+ !ifndef UpdateShortcutAppModelIDs
+ !insertmacro GetLongPath
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define UpdateShortcutAppModelIDs "!insertmacro UpdateShortcutAppModelIDsCall"
+
+ Function UpdateShortcutAppModelIDs
+ ; stack: path, appid
+ Exch $R9 ; stack: $R9, appid | $R9 = path
+ Exch 1 ; stack: appid, $R9
+ Exch $R8 ; stack: $R8, $R9 | $R8 = appid
+ Push $R7 ; stack: $R7, $R8, $R9
+ Push $R6
+ Push $R5
+ Push $R4
+ Push $R3 ; stack: $R3, $R5, $R6, $R7, $R8, $R9
+ Push $R2
+
+ ; $R9 = main application executable path
+ ; $R8 = appid
+ ; $R7 = path to the application's start menu programs directory
+ ; $R6 = path to the shortcut log ini file
+ ; $R5 = shortcut filename
+ ; $R4 = GetShortCutTarget result
+
+ StrCpy $R3 "false"
+
+ ${If} ${AtLeastWin7}
+ ; installed shortcuts
+ ${${_MOZFUNC_UN}GetLongPath} "$INSTDIR\uninstall\${SHORTCUTS_LOG}" $R6
+ ${If} ${FileExists} "$R6"
+ ; Update the Start Menu shortcuts' App ID for this application
+ StrCpy $R2 -1
+ ${Do}
+ IntOp $R2 $R2 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R5 "$R6" "STARTMENU" "Shortcut$R2"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$SMPROGRAMS\$R5"
+ ShellLink::GetShortCutTarget "$SMPROGRAMS\$$R5"
+ Pop $R4
+ ${GetLongPath} "$R4" $R4
+ ${If} "$R4" == "$R9" ; link path == install path
+ ApplicationID::Set "$SMPROGRAMS\$R5" "$R8"
+ Pop $R4
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ; Update the Quick Launch shortcuts' App ID for this application
+ StrCpy $R2 -1
+ ${Do}
+ IntOp $R2 $R2 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R5 "$R6" "QUICKLAUNCH" "Shortcut$R2"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$QUICKLAUNCH\$R5"
+ ShellLink::GetShortCutTarget "$QUICKLAUNCH\$R5"
+ Pop $R4
+ ${GetLongPath} "$R4" $R4
+ ${If} "$R4" == "$R9" ; link path == install path
+ ApplicationID::Set "$QUICKLAUNCH\$R5" "$R8"
+ Pop $R4
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+
+ ; Update the Start Menu Programs shortcuts' App ID for this application
+ ClearErrors
+ ReadINIStr $R7 "$R6" "SMPROGRAMS" "RelativePathToDir"
+ ${Unless} ${Errors}
+ ${${_MOZFUNC_UN}GetLongPath} "$SMPROGRAMS\$R7" $R7
+ ${Unless} "$R7" == ""
+ StrCpy $R2 -1
+ ${Do}
+ IntOp $R2 $R2 + 1 ; Increment the counter
+ ClearErrors
+ ReadINIStr $R5 "$R6" "SMPROGRAMS" "Shortcut$R2"
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+
+ ${If} ${FileExists} "$R7\$R5"
+ ShellLink::GetShortCutTarget "$R7\$R5"
+ Pop $R4
+ ${GetLongPath} "$R4" $R4
+ ${If} "$R4" == "$R9" ; link path == install path
+ ApplicationID::Set "$R7\$R5" "$R8"
+ Pop $R4
+ ${EndIf}
+ ${EndIf}
+ ${Loop}
+ ${EndUnless}
+ ${EndUnless}
+ ${EndIf}
+
+ StrCpy $R7 "$QUICKLAUNCH\User Pinned"
+ StrCpy $R3 "false"
+
+ ; $R9 = main application executable path
+ ; $R8 = appid
+ ; $R7 = user pinned path
+ ; $R6 = find handle
+ ; $R5 = found filename
+ ; $R4 = GetShortCutTarget result
+
+ ; TaskBar links
+ FindFirst $R6 $R5 "$R7\TaskBar\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$R7\TaskBar\$R5"
+ ShellLink::GetShortCutTarget "$R7\TaskBar\$R5"
+ Pop $R4
+ ${If} "$R4" == "$R9" ; link path == install path
+ ApplicationID::Set "$R7\TaskBar\$R5" "$R8"
+ Pop $R4 ; pop Set result off the stack
+ StrCpy $R3 "true"
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R6 $R5
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R6
+
+ ; Start menu links
+ FindFirst $R6 $R5 "$R7\StartMenu\*.lnk"
+ ${Do}
+ ${If} ${FileExists} "$R7\StartMenu\$R5"
+ ShellLink::GetShortCutTarget "$R7\StartMenu\$R5"
+ Pop $R4
+ ${If} "$R4" == "$R9" ; link path == install path
+ ApplicationID::Set "$R7\StartMenu\$R5" "$R8"
+ Pop $R4 ; pop Set result off the stack
+ StrCpy $R3 "true"
+ ${EndIf}
+ ${EndIf}
+ ClearErrors
+ FindNext $R6 $R5
+ ${If} ${Errors}
+ ${ExitDo}
+ ${EndIf}
+ ${Loop}
+ FindClose $R6
+ ${EndIf}
+
+ ClearErrors
+
+ StrCpy $R9 $R3
+
+ Pop $R2
+ Pop $R3 ; stack: $R4, $R5, $R6, $R7, $R8, $R9
+ Pop $R4 ; stack: $R5, $R6, $R7, $R8, $R9
+ Pop $R5 ; stack: $R6, $R7, $R8, $R9
+ Pop $R6 ; stack: $R7, $R8, $R9
+ Pop $R7 ; stack: $R8, $R9
+ Exch $R8 ; stack: appid, $R9 | $R8 = old $R8
+ Exch 1 ; stack: $R9, appid
+ Exch $R9 ; stack: path, appid | $R9 = old $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro UpdateShortcutAppModelIDsCall _EXE_PATH _APP_ID _RESULT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_APP_ID}"
+ Push "${_EXE_PATH}"
+ Call UpdateShortcutAppModelIDs
+ Pop ${_RESULT}
+ !verbose pop
+!macroend
+
+!macro IsUserAdmin
+ ; Copied from: http://nsis.sourceforge.net/IsUserAdmin
+ Function IsUserAdmin
+ Push $R0
+ Push $R1
+ Push $R2
+
+ ClearErrors
+ UserInfo::GetName
+ IfErrors Win9x
+ Pop $R1
+ UserInfo::GetAccountType
+ Pop $R2
+
+ StrCmp $R2 "Admin" 0 Continue
+ StrCpy $R0 "true"
+ Goto Done
+
+ Continue:
+
+ StrCmp $R2 "" Win9x
+ StrCpy $R0 "false"
+ Goto Done
+
+ Win9x:
+ StrCpy $R0 "true"
+
+ Done:
+ Pop $R2
+ Pop $R1
+ Exch $R0
+ FunctionEnd
+!macroend
+
+/**
+ * Retrieve if present or generate and store a 64 bit hash of an install path
+ * using the City Hash algorithm. On return the resulting id is saved in the
+ * $AppUserModelID variable declared by inserting this macro. InitHashAppModelId
+ * will attempt to load from HKLM/_REG_PATH first, then HKCU/_REG_PATH. If found
+ * in either it will return the hash it finds. If not found it will generate a
+ * new hash and attempt to store the hash in HKLM/_REG_PATH, then HKCU/_REG_PATH.
+ * Subsequent calls will then retreive the stored hash value. On any failure,
+ * $AppUserModelID will be set to an empty string.
+ *
+ * Registry format: root/_REG_PATH/"_EXE_PATH" = "hash"
+ *
+ * @param _EXE_PATH
+ * The main application executable path
+ * @param _REG_PATH
+ * The HKLM/HKCU agnostic registry path where the key hash should
+ * be stored. ex: "Software\Mozilla\Firefox\TaskBarIDs"
+ * @result (Var) $AppUserModelID contains the app model id.
+ */
+!macro InitHashAppModelId
+ !ifndef ${_MOZFUNC_UN}InitHashAppModelId
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}GetLongPath
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !ifndef InitHashAppModelId
+ Var AppUserModelID
+ !endif
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}InitHashAppModelId "!insertmacro ${_MOZFUNC_UN}InitHashAppModelIdCall"
+
+ Function ${_MOZFUNC_UN}InitHashAppModelId
+ ; stack: apppath, regpath
+ Exch $R9 ; stack: $R9, regpath | $R9 = apppath
+ Exch 1 ; stack: regpath, $R9
+ Exch $R8 ; stack: $R8, $R9 | $R8 = regpath
+ Push $R7
+
+ ${If} ${AtLeastWin7}
+ ${${_MOZFUNC_UN}GetLongPath} "$R9" $R9
+ ; Always create a new AppUserModelID and overwrite the existing one
+ ; for the current installation path.
+ CityHash::GetCityHash64 "$R9"
+ Pop $AppUserModelID
+ ${If} $AppUserModelID == "error"
+ GoTo end
+ ${EndIf}
+ ClearErrors
+ WriteRegStr HKLM "$R8" "$R9" "$AppUserModelID"
+ ${If} ${Errors}
+ ClearErrors
+ WriteRegStr HKCU "$R8" "$R9" "$AppUserModelID"
+ ${If} ${Errors}
+ StrCpy $AppUserModelID "error"
+ ${EndIf}
+ ${EndIf}
+ ${EndIf}
+
+ end:
+ ${If} "$AppUserModelID" == "error"
+ StrCpy $AppUserModelID ""
+ ${EndIf}
+
+ ClearErrors
+ Pop $R7
+ Exch $R8
+ Exch 1
+ Exch $R9
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro InitHashAppModelIdCall _EXE_PATH _REG_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REG_PATH}"
+ Push "${_EXE_PATH}"
+ Call InitHashAppModelId
+ !verbose pop
+!macroend
+
+!macro un.InitHashAppModelIdCall _EXE_PATH _REG_PATH
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_REG_PATH}"
+ Push "${_EXE_PATH}"
+ Call un.InitHashAppModelId
+ !verbose pop
+!macroend
+
+!macro un.InitHashAppModelId
+ !ifndef un.InitHashAppModelId
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro InitHashAppModelId
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+################################################################################
+# Helpers for taskbar progress
+
+!ifndef CLSCTX_INPROC_SERVER
+ !define CLSCTX_INPROC_SERVER 1
+!endif
+
+!define CLSID_ITaskbarList {56fdf344-fd6d-11d0-958a-006097c9a090}
+!define IID_ITaskbarList3 {ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf}
+!define ITaskbarList3->SetProgressValue $ITaskbarList3->9
+!define ITaskbarList3->SetProgressState $ITaskbarList3->10
+
+/**
+ * Creates a single uninitialized object of the ITaskbarList class with a
+ * reference to the ITaskbarList3 interface. This object can be used to set
+ * progress and state on the installer's taskbar icon using the helper macros
+ * in this section.
+ */
+!macro ITBL3Create
+
+ !ifndef ${_MOZFUNC_UN}ITBL3Create
+ Var ITaskbarList3
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}ITBL3Create "!insertmacro ${_MOZFUNC_UN}ITBL3CreateCall"
+
+ Function ${_MOZFUNC_UN}ITBL3Create
+ ; Setting to 0 allows the helper macros to detect when the object was not
+ ; created.
+ StrCpy $ITaskbarList3 0
+ ; Don't create when running silently.
+ ${Unless} ${Silent}
+ ; This is only supported on Win 7 and above.
+ ${If} ${AtLeastWin7}
+ System::Call "ole32::CoCreateInstance(g '${CLSID_ITaskbarList}', \
+ i 0, \
+ i ${CLSCTX_INPROC_SERVER}, \
+ g '${IID_ITaskbarList3}', \
+ *i .s)"
+ Pop $ITaskbarList3
+ ${EndIf}
+ ${EndUnless}
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro ITBL3CreateCall
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call ITBL3Create
+ !verbose pop
+!macroend
+
+!macro un.ITBL3CreateCall _PATH_TO_IMAGE
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Call un.ITBL3Create
+ !verbose pop
+!macroend
+
+!macro un.ITBL3Create
+ !ifndef un.ITBL3Create
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro ITBL3Create
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Sets the percentage completed on the taskbar process icon progress indicator.
+ *
+ * @param _COMPLETED
+ * The proportion of the operation that has been completed in relation
+ * to _TOTAL.
+ * @param _TOTAL
+ * The value _COMPLETED will have when the operation has completed.
+ *
+ * $R8 = _COMPLETED
+ * $R9 = _TOTAL
+ */
+!macro ITBL3SetProgressValueCall _COMPLETED _TOTAL
+ Push ${_COMPLETED}
+ Push ${_TOTAL}
+ ${CallArtificialFunction} ITBL3SetProgressValue_
+!macroend
+
+!define ITBL3SetProgressValue "!insertmacro ITBL3SetProgressValueCall"
+!define un.ITBL3SetProgressValue "!insertmacro ITBL3SetProgressValueCall"
+
+!macro ITBL3SetProgressValue_
+ Exch $R9
+ Exch 1
+ Exch $R8
+ ${If} $ITaskbarList3 <> 0
+ System::Call "${ITaskbarList3->SetProgressValue}(i$HWNDPARENT, l$R8, l$R9)"
+ ${EndIf}
+ Exch $R8
+ Exch 1
+ Exch $R9
+!macroend
+
+; Normal state / no progress bar
+!define TBPF_NOPROGRESS 0x00000000
+; Marquee style progress bar
+!define TBPF_INDETERMINATE 0x00000001
+; Standard progress bar
+!define TBPF_NORMAL 0x00000002
+; Red taskbar button to indicate an error occurred
+!define TBPF_ERROR 0x00000004
+; Yellow taskbar button to indicate user attention (input) is required to
+; resume progress
+!define TBPF_PAUSED 0x00000008
+
+/**
+ * Sets the state on the taskbar process icon progress indicator.
+ *
+ * @param _STATE
+ * The state to set on the taskbar icon progress indicator. Only one of
+ * the states defined above should be specified.
+ *
+ * $R9 = _STATE
+ */
+!macro ITBL3SetProgressStateCall _STATE
+ Push ${_STATE}
+ ${CallArtificialFunction} ITBL3SetProgressState_
+!macroend
+
+!define ITBL3SetProgressState "!insertmacro ITBL3SetProgressStateCall"
+!define un.ITBL3SetProgressState "!insertmacro ITBL3SetProgressStateCall"
+
+!macro ITBL3SetProgressState_
+ Exch $R9
+ ${If} $ITaskbarList3 <> 0
+ System::Call "${ITaskbarList3->SetProgressState}(i$HWNDPARENT, i$R9)"
+ ${EndIf}
+ Exch $R9
+!macroend
+
+################################################################################
+# Helpers for the new user interface
+
+!ifndef MAXDWORD
+ !define MAXDWORD 0xffffffff
+!endif
+
+!ifndef DT_WORDBREAK
+ !define DT_WORDBREAK 0x0010
+!endif
+!ifndef DT_SINGLELINE
+ !define DT_SINGLELINE 0x0020
+!endif
+!ifndef DT_NOCLIP
+ !define DT_NOCLIP 0x0100
+!endif
+!ifndef DT_CALCRECT
+ !define DT_CALCRECT 0x0400
+!endif
+!ifndef DT_EDITCONTROL
+ !define DT_EDITCONTROL 0x2000
+!endif
+!ifndef DT_RTLREADING
+ !define DT_RTLREADING 0x00020000
+!endif
+!ifndef DT_NOFULLWIDTHCHARBREAK
+ !define DT_NOFULLWIDTHCHARBREAK 0x00080000
+!endif
+
+!ifndef WS_EX_NOINHERITLAYOUT
+ !define WS_EX_NOINHERITLAYOUT 0x00100000
+!endif
+!ifndef WS_EX_LAYOUTRTL
+ !define WS_EX_LAYOUTRTL 0x00400000
+!endif
+
+!ifndef PBS_MARQUEE
+ !define PBS_MARQUEE 0x08
+!endif
+
+!ifndef PBM_SETRANGE32
+ !define PBM_SETRANGE32 0x406
+!endif
+!ifndef PBM_GETRANGE
+ !define PBM_GETRANGE 0x407
+!endif
+
+!ifndef SHACF_FILESYSTEM
+ !define SHACF_FILESYSTEM 1
+!endif
+
+!define MOZ_LOADTRANSPARENT ${LR_LOADFROMFILE}|${LR_LOADTRANSPARENT}|${LR_LOADMAP3DCOLORS}
+
+; Extend nsDialogs.nsh to support creating centered labels if it is already
+; included
+!ifmacrodef __NSD_DefineControl
+!insertmacro __NSD_DefineControl LabelCenter
+!define __NSD_LabelCenter_CLASS STATIC
+!define __NSD_LabelCenter_STYLE ${DEFAULT_STYLES}|${SS_NOTIFY}|${SS_CENTER}
+!define __NSD_LabelCenter_EXSTYLE ${WS_EX_TRANSPARENT}
+!endif
+
+/**
+ * Modified version of the __NSD_SetStretchedImage macro from nsDialogs.nsh that
+ * supports transparency. See nsDialogs documentation for additional info.
+ */
+!macro __SetStretchedTransparentImage CONTROL IMAGE HANDLE
+ Push $0
+ Push $1
+ Push $2
+ Push $R0
+
+ StrCpy $R0 ${CONTROL} ; in case ${CONTROL} is $0
+ StrCpy $1 ""
+ StrCpy $2 ""
+
+ System::Call '*(i, i, i, i) i.s'
+ Pop $0
+
+ ${If} $0 <> 0
+ System::Call 'user32::GetClientRect(i R0, i r0)'
+ System::Call '*$0(i, i, i .s, i .s)'
+ System::Free $0
+ Pop $1
+ Pop $2
+ ${EndIf}
+
+ System::Call 'user32::LoadImageW(i 0, t s, i ${IMAGE_BITMAP}, i r1, i r2, \
+ i ${MOZ_LOADTRANSPARENT}) i .s' "${IMAGE}"
+ Pop $0
+ SendMessage $R0 ${STM_SETIMAGE} ${IMAGE_BITMAP} $0
+
+ SetCtlColors $R0 "" transparent
+ ${NSD_AddExStyle} $R0 ${WS_EX_TRANSPARENT}|${WS_EX_TOPMOST}
+
+ Pop $R0
+ Pop $2
+ Pop $1
+ Exch $0
+ Pop ${HANDLE}
+!macroend
+!define SetStretchedTransparentImage "!insertmacro __SetStretchedTransparentImage"
+
+/**
+ * Removes a single style from a control.
+ *
+ * _HANDLE the handle of the control
+ * _STYLE the style to remove
+ */
+!macro _RemoveStyle _HANDLE _STYLE
+ Push $0
+
+ System::Call 'user32::GetWindowLongW(i ${_HANDLE}, i ${GWL_STYLE}) i .r0'
+ IntOp $0 $0 | ${_STYLE}
+ IntOp $0 $0 - ${_STYLE}
+ System::Call 'user32::SetWindowLongW(i ${_HANDLE}, i ${GWL_STYLE}, i r0)'
+
+ Pop $0
+!macroend
+!define RemoveStyle "!insertmacro _RemoveStyle"
+
+/**
+ * Removes a single extended style from a control.
+ *
+ * _HANDLE the handle of the control
+ * _EXSTYLE the extended style to remove
+ */
+!macro _RemoveExStyle _HANDLE _EXSTYLE
+ Push $0
+
+ System::Call 'user32::GetWindowLongW(i ${_HANDLE}, i ${GWL_EXSTYLE}) i .r0'
+ IntOp $0 $0 | ${_EXSTYLE}
+ IntOp $0 $0 - ${_EXSTYLE}
+ System::Call 'user32::SetWindowLongW(i ${_HANDLE}, i ${GWL_EXSTYLE}, i r0)'
+
+ Pop $0
+!macroend
+!define RemoveExStyle "!insertmacro _RemoveExStyle"
+
+/**
+ * Gets the extent of the specified text in pixels for sizing a control.
+ *
+ * _TEXT the text to get the text extent for
+ * _FONT the font to use when getting the text extent
+ * _RES_WIDTH return value - control width for the text
+ * _RES_HEIGHT return value - control height for the text
+ */
+!macro GetTextExtentCall _TEXT _FONT _RES_WIDTH _RES_HEIGHT
+ Push "${_TEXT}"
+ Push "${_FONT}"
+ ${CallArtificialFunction} GetTextExtent_
+ Pop ${_RES_WIDTH}
+ Pop ${_RES_HEIGHT}
+!macroend
+
+!define GetTextExtent "!insertmacro GetTextExtentCall"
+!define un.GetTextExtent "!insertmacro GetTextExtentCall"
+
+!macro GetTextExtent_
+ Exch $0 ; font
+ Exch 1
+ Exch $1 ; text
+ Push $2
+ Push $3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+
+ ; Reuse the existing NSIS control which is used for BrandingText instead of
+ ; creating a new control.
+ GetDlgItem $2 $HWNDPARENT 1028
+
+ System::Call 'user32::GetDC(i r2) i .r3'
+ System::Call 'gdi32::SelectObject(i r3, i r0)'
+
+ StrLen $4 "$1"
+
+ System::Call '*(i, i) i .r5'
+ System::Call 'gdi32::GetTextExtentPoint32W(i r3, t$\"$1$\", i r4, i r5)'
+ System::Call '*$5(i .r6, i .r7)'
+ System::Free $5
+
+ System::Call 'user32::ReleaseDC(i r2, i r3)'
+
+ StrCpy $1 $7
+ StrCpy $0 $6
+
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Pop $2
+ Exch $1 ; return height
+ Exch 1
+ Exch $0 ; return width
+!macroend
+
+/**
+ * Gets the width and the height of a control in pixels.
+ *
+ * _HANDLE the handle of the control
+ * _RES_WIDTH return value - control width for the text
+ * _RES_HEIGHT return value - control height for the text
+ */
+!macro GetDlgItemWidthHeightCall _HANDLE _RES_WIDTH _RES_HEIGHT
+ Push "${_HANDLE}"
+ ${CallArtificialFunction} GetDlgItemWidthHeight_
+ Pop ${_RES_WIDTH}
+ Pop ${_RES_HEIGHT}
+!macroend
+
+!define GetDlgItemWidthHeight "!insertmacro GetDlgItemWidthHeightCall"
+!define un.GetDlgItemWidthHeight "!insertmacro GetDlgItemWidthHeightCall"
+
+!macro GetDlgItemWidthHeight_
+ Exch $0 ; handle for the control
+ Push $1
+ Push $2
+
+ System::Call '*(i, i, i, i) i .r2'
+ ; The left and top values will always be 0 so the right and bottom values
+ ; will be the width and height.
+ System::Call 'user32::GetClientRect(i r0, i r2)'
+ System::Call '*$2(i, i, i .r0, i .r1)'
+ System::Free $2
+
+ Pop $2
+ Exch $1 ; return height
+ Exch 1
+ Exch $0 ; return width
+!macroend
+
+/**
+ * Gets the number of pixels from the beginning of the dialog to the end of a
+ * control in a RTL friendly manner.
+ *
+ * _HANDLE the handle of the control
+ * _RES_PX return value - pixels from the beginning of the dialog to the end of
+ * the control
+ */
+!macro GetDlgItemEndPXCall _HANDLE _RES_PX
+ Push "${_HANDLE}"
+ ${CallArtificialFunction} GetDlgItemEndPX_
+ Pop ${_RES_PX}
+!macroend
+
+!define GetDlgItemEndPX "!insertmacro GetDlgItemEndPXCall"
+!define un.GetDlgItemEndPX "!insertmacro GetDlgItemEndPXCall"
+
+!macro GetDlgItemEndPX_
+ Exch $0 ; handle of the control
+ Push $1
+ Push $2
+
+ ; #32770 is the dialog class
+ FindWindow $1 "#32770" "" $HWNDPARENT
+ System::Call '*(i, i, i, i) i .r2'
+ System::Call 'user32::GetWindowRect(i r0, i r2)'
+ System::Call 'user32::MapWindowPoints(i 0, i r1,i r2, i 2)'
+ System::Call '*$2(i, i, i .r0, i)'
+ System::Free $2
+
+ Pop $2
+ Pop $1
+ Exch $0 ; pixels from the beginning of the dialog to the end of the control
+!macroend
+
+/**
+ * Gets the width and height for sizing a control that has the specified text.
+ * If the text has embedded newlines then the width and height will be
+ * determined without trying to optimize the control's width and height. If the
+ * text doesn't contain newlines the control's height and width will be
+ * dynamically determined using a minimum of 3 lines (incrementing the
+ * number of lines if necessary) for the height and the maximum width specified.
+ *
+ * _TEXT the text
+ * _FONT the font to use when getting the width and height
+ * _MAX_WIDTH the maximum width for the control
+ * _RES_WIDTH return value - control width for the text
+ * _RES_HEIGHT return value - control height for the text
+ */
+!macro GetTextWidthHeight
+
+ !ifndef ${_MOZFUNC_UN}GetTextWidthHeight
+ !define _MOZFUNC_UN_TMP ${_MOZFUNC_UN}
+ !insertmacro ${_MOZFUNC_UN_TMP}WordFind
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN ${_MOZFUNC_UN_TMP}
+ !undef _MOZFUNC_UN_TMP
+
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !define ${_MOZFUNC_UN}GetTextWidthHeight "!insertmacro ${_MOZFUNC_UN}GetTextWidthHeightCall"
+
+ Function ${_MOZFUNC_UN}GetTextWidthHeight
+ Exch $0 ; maximum width use to calculate the control's width and height
+ Exch 1
+ Exch $1 ; font
+ Exch 2
+ Exch $2 ; text
+ Push $3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+ Push $8
+ Push $9
+ Push $R0
+ Push $R1
+ Push $R2
+
+ StrCpy $R2 "${DT_NOCLIP}|${DT_CALCRECT}"
+ !ifdef ${AB_CD}_rtl
+ StrCpy $R2 "$R2|${DT_RTLREADING}"
+ !endif
+
+ ; Reuse the existing NSIS control which is used for BrandingText instead
+ ; of creating a new control.
+ GetDlgItem $3 $HWNDPARENT 1028
+
+ System::Call 'user32::GetDC(i r3) i .r4'
+ System::Call 'gdi32::SelectObject(i r4, i r1)'
+
+ StrLen $5 "$2" ; text length
+ System::Call '*(i, i, i, i) i .r6'
+
+ ClearErrors
+ ${${_MOZFUNC_UN}WordFind} "$2" "$\n" "E#" $R0
+ ${If} ${Errors}
+ ; When there aren't newlines in the text calculate the size of the
+ ; rectangle needed for the text with a minimum of three lines of text.
+ ClearErrors
+ System::Call 'user32::DrawTextW(i r4, t $\"$2$\", i r5, i r6, \
+ i $R2|${DT_SINGLELINE})'
+ System::Call '*$6(i, i, i .r8, i .r7)'
+ System::Free $6
+
+ ; Get the approximate number height needed to display the text starting
+ ; with a minimum of 3 lines of text.
+ StrCpy $9 $8
+ StrCpy $R1 2 ; set the number of lines initially to 2
+ ${Do}
+ IntOp $R1 $R1 + 1 ; increment the number of lines
+ IntOp $9 $8 / $R1
+ ${LoopUntil} $9 < $0
+ IntOp $7 $7 * $R1
+
+ StrCpy $R0 $9
+ ${Do}
+ IntOp $R0 $R0 + 20
+ System::Call '*(i, i, i R0, i r7) i .r6'
+ System::Call 'user32::DrawTextW(i r4, t $\"$2$\", i r5, i r6, \
+ i $R2|${DT_WORDBREAK}) i .R1'
+ System::Call '*$6(i, i, i .r8, i .r9)'
+ System::Free $6
+ ${LoopUntil} $7 >= $R1
+ ${Else}
+ ; When there are newlines in the text just return the size of the
+ ; rectangle for the text.
+ System::Call 'user32::DrawTextW(i r4, t $\"$2$\", i r5, i r6, i $R2)'
+ System::Call '*$6(i, i, i .r8, i .r9)'
+ System::Free $6
+ ${EndIf}
+
+ ; Reselect the original DC
+ System::Call 'gdi32::SelectObject(i r4, i r1)'
+ System::Call 'user32::ReleaseDC(i r3, i r4)'
+
+ StrCpy $1 $9
+ StrCpy $0 $8
+
+ Pop $R2
+ Pop $R1
+ Pop $R0
+ Pop $9
+ Pop $8
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Exch $2
+ Exch 2
+ Exch $1 ; return height
+ Exch 1
+ Exch $0 ; return width
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro GetTextWidthHeightCall _TEXT _FONT _MAX_WIDTH _RES_WIDTH _RES_HEIGHT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_TEXT}"
+ Push "${_FONT}"
+ Push "${_MAX_WIDTH}"
+ Call GetTextWidthHeight
+ Pop ${_RES_WIDTH}
+ Pop ${_RES_HEIGHT}
+ !verbose pop
+!macroend
+
+!macro un.GetTextWidthHeightCall _TEXT _FONT _MAX_WIDTH _RES_WIDTH _RES_HEIGHT
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ Push "${_TEXT}"
+ Push "${_FONT}"
+ Push "${_MAX_WIDTH}"
+ Call un.GetTextWidthHeight
+ Pop ${_RES_WIDTH}
+ Pop ${_RES_HEIGHT}
+ !verbose pop
+!macroend
+
+!macro un.GetTextWidthHeight
+ !ifndef un.GetTextWidthHeight
+ !verbose push
+ !verbose ${_MOZFUNC_VERBOSE}
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN "un."
+
+ !insertmacro GetTextWidthHeight
+
+ !undef _MOZFUNC_UN
+ !define _MOZFUNC_UN
+ !verbose pop
+ !endif
+!macroend
+
+/**
+ * Gets the elapsed time in seconds between two values in milliseconds stored as
+ * an int64. The caller will typically get the millisecond values using
+ * GetTickCount with a long return value as follows.
+ * System::Call "kernel32::GetTickCount()l .s"
+ * Pop $varname
+ *
+ * _START_TICK_COUNT
+ * _FINISH_TICK_COUNT
+ * _RES_ELAPSED_SECONDS return value - elapsed time between _START_TICK_COUNT
+ * and _FINISH_TICK_COUNT in seconds.
+ */
+!macro GetSecondsElapsedCall _START_TICK_COUNT _FINISH_TICK_COUNT _RES_ELAPSED_SECONDS
+ Push "${_START_TICK_COUNT}"
+ Push "${_FINISH_TICK_COUNT}"
+ ${CallArtificialFunction} GetSecondsElapsed_
+ Pop ${_RES_ELAPSED_SECONDS}
+!macroend
+
+!define GetSecondsElapsed "!insertmacro GetSecondsElapsedCall"
+!define un.GetSecondsElapsed "!insertmacro GetSecondsElapsedCall"
+
+!macro GetSecondsElapsed_
+ Exch $0 ; finish tick count
+ Exch 1
+ Exch $1 ; start tick count
+
+ System::Int64Op $0 - $1
+ Pop $0
+ ; Discard the top bits of the int64 by bitmasking with MAXDWORD
+ System::Int64Op $0 & ${MAXDWORD}
+ Pop $0
+
+ ; Convert from milliseconds to seconds
+ System::Int64Op $0 / 1000
+ Pop $0
+
+ Pop $1
+ Exch $0 ; return elapsed seconds
+!macroend
+
diff --git a/toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh b/toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh
new file mode 100644
index 000000000..4149deadb
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/locale-fonts.nsh
@@ -0,0 +1,681 @@
+# 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/.
+
+; Acholi
+!if "${AB_CD}" == "ach"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Afrikaans
+!if "${AB_CD}" == "af"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Akan
+!if "${AB_CD}" == "ak"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Aragonese
+!if "${AB_CD}" == "an"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Arabic
+!if "${AB_CD}" == "ar"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Assamese
+!if "${AB_CD}" == "as"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Asturian
+!if "${AB_CD}" == "ast"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Azerbaijani
+!if "${AB_CD}" == "az"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Belarusian
+!if "${AB_CD}" == "be"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bulgarian
+!if "${AB_CD}" == "bg"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bengali
+!if "${AB_CD}" == "bn-BD"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Bengali - India
+!if "${AB_CD}" == "bn-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Breton
+!if "${AB_CD}" == "br"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Bosnian
+!if "${AB_CD}" == "bs"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Catalan
+!if "${AB_CD}" == "ca"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Czech
+!if "${AB_CD}" == "cs"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kashubian
+!if "${AB_CD}" == "csb"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Welsh
+!if "${AB_CD}" == "cy"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Danish
+!if "${AB_CD}" == "da"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; German
+!if "${AB_CD}" == "de"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Greek
+!if "${AB_CD}" == "el"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - Great Britain
+!if "${AB_CD}" == "en-GB"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - United States
+!if "${AB_CD}" == "en-US"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; English - South Africa
+!if "${AB_CD}" == "en-ZA"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Esperanto
+!if "${AB_CD}" == "eo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Argentina
+!if "${AB_CD}" == "es-AR"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Chile
+!if "${AB_CD}" == "es-CL"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish
+!if "${AB_CD}" == "es-ES"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Spanish - Mexico
+!if "${AB_CD}" == "es-MX"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Estonian
+!if "${AB_CD}" == "et"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Basque
+!if "${AB_CD}" == "eu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Persian
+!if "${AB_CD}" == "fa"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Fulah
+!if "${AB_CD}" == "ff"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Finnish
+!if "${AB_CD}" == "fi"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; French
+!if "${AB_CD}" == "fr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Frisian
+!if "${AB_CD}" == "fy-NL"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Irish
+!if "${AB_CD}" == "ga-IE"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Scottish Gaelic
+!if "${AB_CD}" == "gd"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Galician
+!if "${AB_CD}" == "gl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Gujarati
+!if "${AB_CD}" == "gu-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Hawaiian
+!if "${AB_CD}" == "haw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Hebrew
+!if "${AB_CD}" == "he"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; hindi
+!if "${AB_CD}" == "hi-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Croatian
+!if "${AB_CD}" == "hr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Upper Sorbian
+!if "${AB_CD}" == "hsb"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Hungarian
+!if "${AB_CD}" == "hu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Armenian
+!if "${AB_CD}" == "hy-AM"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Indonesian
+!if "${AB_CD}" == "id"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Iloko
+!if "${AB_CD}" == "ilo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Icelandic
+!if "${AB_CD}" == "is"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Italian
+!if "${AB_CD}" == "it"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Japanese
+!if "${AB_CD}" == "ja"
+!define FONT_NAME1 "Meiryo UI"
+!define FONT_FILE1 "meiryo.ttc"
+!endif
+
+; Georgian
+!if "${AB_CD}" == "ka"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kazakh
+!if "${AB_CD}" == "kk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Khmer
+!if "${AB_CD}" == "km"
+!define FONT_NAME1 "Leelawadee UI"
+!define FONT_FILE1 "LeelawUI.ttf"
+!endif
+
+; Kannada
+!if "${AB_CD}" == "kn"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Korean
+!if "${AB_CD}" == "ko"
+!define FONT_NAME1 "Malgun Gothic"
+!define FONT_FILE1 "malgun.ttf"
+!endif
+
+; Kurdish
+!if "${AB_CD}" == "ku"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Luganda
+!if "${AB_CD}" == "lg"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Ligurian
+!if "${AB_CD}" == "lij"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Lithuanian
+!if "${AB_CD}" == "lt"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Latvian
+!if "${AB_CD}" == "lv"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Maithili
+!if "${AB_CD}" == "mai"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Macedonian
+!if "${AB_CD}" == "mk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Malayalam
+!if "${AB_CD}" == "ml"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Mongolian
+!if "${AB_CD}" == "mn"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Marathi
+!if "${AB_CD}" == "mr"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Malay
+!if "${AB_CD}" == "ms"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Burmese
+!if "${AB_CD}" == "my"
+!define FONT_NAME1 "Myanmar Text"
+!define FONT_FILE1 "mmrtext.ttf"
+!endif
+
+; Norwegian Bokmål
+!if "${AB_CD}" == "nb-NO"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Nepali Nepal
+!if "${AB_CD}" == "ne-NP"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Dutch
+!if "${AB_CD}" == "nl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Norwegian Nynorsk
+!if "${AB_CD}" == "nn-NO"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Southern Ndebele
+!if "${AB_CD}" == "nr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Northern Sotho
+!if "${AB_CD}" == "nso"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Occitan
+!if "${AB_CD}" == "oc"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Odia
+!if "${AB_CD}" == "or"
+!define FONT_NAME1 "Kalinga"
+!define FONT_FILE1 "kalinga.ttf"
+!endif
+
+; Punjabi
+!if "${AB_CD}" == "pa-IN"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Polish
+!if "${AB_CD}" == "pl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Portugese - Brazil
+!if "${AB_CD}" == "pt-BR"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Portugese
+!if "${AB_CD}" == "pt-PT"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Romansh
+!if "${AB_CD}" == "rm"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Romanian
+!if "${AB_CD}" == "ro"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Russian
+!if "${AB_CD}" == "ru"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Kinyarwanda
+!if "${AB_CD}" == "rw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Sakha
+!if "${AB_CD}" == "sah"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Sinhala
+!if "${AB_CD}" == "si"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Slovak
+!if "${AB_CD}" == "sk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Slovene
+!if "${AB_CD}" == "sl"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Songhay
+!if "${AB_CD}" == "son"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Albanian
+!if "${AB_CD}" == "sq"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Serbian
+!if "${AB_CD}" == "sr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swazi
+!if "${AB_CD}" == "ss"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Southern Sotho
+!if "${AB_CD}" == "st"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swedish
+!if "${AB_CD}" == "sv-Se"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Swahili
+!if "${AB_CD}" == "sw"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Tamil
+!if "${AB_CD}" == "ta"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Tamil - Sri Lanka
+!if "${AB_CD}" == "ta-LK"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Telugu
+!if "${AB_CD}" == "te"
+!define FONT_NAME1 "Nirmala UI"
+!define FONT_FILE1 "Nirmala.ttf"
+!endif
+
+; Thai
+!if "${AB_CD}" == "th"
+!define FONT_NAME1 "Leelawadee UI"
+!define FONT_FILE1 "LeelawUI.ttf"
+!endif
+
+; Tswana
+!if "${AB_CD}" == "tn"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Turkish
+!if "${AB_CD}" == "tr"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Tsonga
+!if "${AB_CD}" == "ts"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Uyghur
+!if "${AB_CD}" == "ug"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Ukrainian
+!if "${AB_CD}" == "uk"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Urdu
+!if "${AB_CD}" == "ur"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Venda
+!if "${AB_CD}" == "ve"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Vietnamese
+!if "${AB_CD}" == "vi"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Wolof
+!if "${AB_CD}" == "wo"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Xhosa
+!if "${AB_CD}" == "xh"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
+
+; Chinese (Simplified)
+!if "${AB_CD}" == "zh-CN"
+!define FONT_NAME1 "Microsoft YaHei UI"
+!define FONT_FILE1 "msyh.ttc"
+!endif
+
+; Chinese (Traditional)
+!if "${AB_CD}" == "zh-TW"
+!define FONT_NAME1 "Microsoft JhengHei UI"
+!define FONT_FILE1 "msjh.ttc"
+!endif
+
+; Zulu
+!if "${AB_CD}" == "zu"
+!define FONT_NAME1 "Segoe UI"
+!define FONT_FILE1 "segoeui.ttf"
+!endif
diff --git a/toolkit/mozapps/installer/windows/nsis/locale-rtl.nlf b/toolkit/mozapps/installer/windows/nsis/locale-rtl.nlf
new file mode 100644
index 000000000..a4ea9ae6e
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/locale-rtl.nlf
@@ -0,0 +1,12 @@
+# Header, don't edit
+NLF v6
+# Start editing here
+# Language ID
+0
+# Font and size - dash (-) means default
+-
+-
+# Codepage - dash (-) means ANSI code page
+-
+# RTL - anything else than RTL means LTR
+RTL
diff --git a/toolkit/mozapps/installer/windows/nsis/locale.nlf b/toolkit/mozapps/installer/windows/nsis/locale.nlf
new file mode 100644
index 000000000..0995026a0
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/locale.nlf
@@ -0,0 +1,12 @@
+# Header, don't edit
+NLF v6
+# Start editing here
+# Language ID
+0
+# Font and size - dash (-) means default
+-
+-
+# Codepage - dash (-) means ANSI code page
+-
+# RTL - anything else than RTL means LTR
+-
diff --git a/toolkit/mozapps/installer/windows/nsis/locales.nsi b/toolkit/mozapps/installer/windows/nsis/locales.nsi
new file mode 100755
index 000000000..938aef47f
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/locales.nsi
@@ -0,0 +1,23 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+/**
+ * "One off" locale configuration settings for RTL (e.g. locale text is read
+ * right to left).
+ */
+
+; Arabic
+!define ar_rtl
+
+; Hebrew
+!define he_rtl
+
+; Persian
+!define fa_rtl
+
+; Uyghur
+!define ug_rtl
+
+; Urdu
+!define ur_rtl
diff --git a/toolkit/mozapps/installer/windows/nsis/makensis.mk b/toolkit/mozapps/installer/windows/nsis/makensis.mk
new file mode 100644
index 000000000..97608e0ce
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/makensis.mk
@@ -0,0 +1,100 @@
+# 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 CONFIG_DIR
+$(error CONFIG_DIR must be set before including makensis.mk)
+endif
+
+include $(MOZILLA_DIR)/toolkit/mozapps/installer/signing.mk
+
+ABS_CONFIG_DIR := $(abspath $(CONFIG_DIR))
+
+SFX_MODULE ?= $(error SFX_MODULE is not defined)
+
+TOOLKIT_NSIS_FILES = \
+ common.nsh \
+ locale.nlf \
+ locale-fonts.nsh \
+ locale-rtl.nlf \
+ locales.nsi \
+ overrides.nsh \
+ setup.ico \
+ $(NULL)
+
+CUSTOM_NSIS_PLUGINS = \
+ AccessControl.dll \
+ AppAssocReg.dll \
+ ApplicationID.dll \
+ CertCheck.dll \
+ CityHash.dll \
+ InetBgDL.dll \
+ InvokeShellVerb.dll \
+ liteFirewallW.dll \
+ ServicesHelper.dll \
+ ShellLink.dll \
+ UAC.dll \
+ $(NULL)
+
+CUSTOM_UI = \
+ nsisui.exe \
+ $(NULL)
+
+$(CONFIG_DIR)/setup.exe::
+ $(INSTALL) $(addprefix $(MOZILLA_DIR)/toolkit/mozapps/installer/windows/nsis/,$(TOOLKIT_NSIS_FILES)) $(CONFIG_DIR)
+ $(INSTALL) $(addprefix $(MOZILLA_DIR)/other-licenses/nsis/Plugins/,$(CUSTOM_NSIS_PLUGINS)) $(CONFIG_DIR)
+ $(INSTALL) $(addprefix $(MOZILLA_DIR)/other-licenses/nsis/,$(CUSTOM_UI)) $(CONFIG_DIR)
+ cd $(CONFIG_DIR) && $(MAKENSISU) installer.nsi
+ifdef MOZ_STUB_INSTALLER
+ cd $(CONFIG_DIR) && $(MAKENSISU) stub.nsi
+ifdef MOZ_EXTERNAL_SIGNING_FORMAT
+ $(MOZ_SIGN_CMD) $(foreach f,$(MOZ_EXTERNAL_SIGNING_FORMAT),-f $(f)) $(CONFIG_DIR)/setup-stub.exe
+endif
+ $(MAKE) $(CONFIG_DIR)/7zSD.sfx
+ cd $(CONFIG_DIR) && $(CYGWIN_WRAPPER) 7z a -t7z $(ABS_CONFIG_DIR)/stub.7z setup-stub.exe -mx -m0=BCJ2 -m1=LZMA:d21 -m2=LZMA:d17 -m3=LZMA:d17 -mb0:1 -mb0s1:2 -mb0s2:3
+ cat $(CONFIG_DIR)/7zSD.sfx $(CONFIG_DIR)/stub.tag $(CONFIG_DIR)/stub.7z > "$(CONFIG_DIR)/stub.exe"
+ifdef MOZ_EXTERNAL_SIGNING_FORMAT_STUB
+ $(MOZ_SIGN_CMD) $(foreach f,$(MOZ_EXTERNAL_SIGNING_FORMAT_STUB),-f $(f)) $(CONFIG_DIR)/stub.exe
+endif
+endif
+# Support for building the uninstaller when repackaging locales
+ifeq ($(CONFIG_DIR),l10ngen)
+ cd $(CONFIG_DIR) && $(MAKENSISU) uninstaller.nsi
+endif
+ifdef MOZ_EXTERNAL_SIGNING_FORMAT
+ $(MOZ_SIGN_CMD) $(foreach f,$(MOZ_EXTERNAL_SIGNING_FORMAT),-f $(f)) "$@"
+endif
+
+$(CONFIG_DIR)/7zSD.sfx:
+ $(CYGWIN_WRAPPER) upx --best -o $(CONFIG_DIR)/7zSD.sfx $(SFX_MODULE)
+
+installer::
+ $(INSTALL) $(CONFIG_DIR)/setup.exe $(DEPTH)/installer-stage
+ cd $(DEPTH)/installer-stage && $(CYGWIN_WRAPPER) 7z a -r -t7z $(ABS_CONFIG_DIR)/app.7z -mx -m0=BCJ2 -m1=LZMA:d25 -m2=LZMA:d19 -m3=LZMA:d19 -mb0:1 -mb0s1:2 -mb0s2:3
+ $(MAKE) $(CONFIG_DIR)/7zSD.sfx
+ $(NSINSTALL) -D $(DIST)/$(PKG_INST_PATH)
+ cat $(CONFIG_DIR)/7zSD.sfx $(CONFIG_DIR)/app.tag $(CONFIG_DIR)/app.7z > "$(DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe"
+ chmod 0755 "$(DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe"
+ifdef MOZ_STUB_INSTALLER
+ cp $(CONFIG_DIR)/stub.exe "$(DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe"
+ chmod 0755 "$(DIST)/$(PKG_INST_PATH)$(PKG_STUB_BASENAME).exe"
+endif
+ifdef MOZ_EXTERNAL_SIGNING_FORMAT
+ $(MOZ_SIGN_CMD) $(foreach f,$(MOZ_EXTERNAL_SIGNING_FORMAT),-f $(f)) "$(DIST)/$(PKG_INST_PATH)$(PKG_INST_BASENAME).exe"
+endif
+
+# For building the uninstaller during the application build so it can be
+# included for mar file generation.
+uninstaller::
+ $(INSTALL) $(addprefix $(MOZILLA_DIR)/toolkit/mozapps/installer/windows/nsis/,$(TOOLKIT_NSIS_FILES)) $(CONFIG_DIR)
+ $(INSTALL) $(addprefix $(MOZILLA_DIR)/other-licenses/nsis/Plugins/,$(CUSTOM_NSIS_PLUGINS)) $(CONFIG_DIR)
+ 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/installer/windows/nsis/overrides.nsh b/toolkit/mozapps/installer/windows/nsis/overrides.nsh
new file mode 100755
index 000000000..5d1f26965
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/overrides.nsh
@@ -0,0 +1,604 @@
+# 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/.
+
+################################################################################
+# Modified versions of macros provided by NSIS
+
+!ifndef OVERRIDES_INCLUDED
+!define OVERRIDES_INCLUDED
+
+!ifndef ___WINVER__NSH___
+!include WinVer.nsh
+!endif
+
+; When including a file check if its verbose macro is defined to prevent
+; loading the file a second time.
+!ifmacrondef TEXTFUNC_VERBOSE
+!include TextFunc.nsh
+!endif
+
+!ifmacrondef FILEFUNC_VERBOSE
+!include FileFunc.nsh
+!endif
+
+!macro __MOZ__WinVer_DefineOSTests WinVer
+ !insertmacro __WinVer_DefineOSTest AtLeast ${WinVer} ""
+ !insertmacro __WinVer_DefineOSTest Is ${WinVer} ""
+ !insertmacro __WinVer_DefineOSTest AtMost ${WinVer} ""
+!macroend
+
+!ifndef WINVER_8
+ !define WINVER_8 0x06020000 ;6.02.9200
+ !insertmacro __MOZ__WinVer_DefineOSTests 8
+!endif
+
+!ifndef WINVER_8.1
+ !define WINVER_8.1 0x06030000 ;6.03.9600
+ !insertmacro __MOZ__WinVer_DefineOSTests 8.1
+!endif
+
+!ifndef WINVER_2012
+ !define WINVER_2012 0x06020001 ;6.02.9200
+ !insertmacro __MOZ__WinVer_DefineOSTests 2012
+!endif
+
+!ifndef WINVER_2012R2
+ !define WINVER_2012R2 0x06030001 ;6.03.9600
+ !insertmacro __MOZ__WinVer_DefineOSTests 2012R2
+!endif
+
+!ifndef WINVER_10
+ !define WINVER_10 0x0A000000 ;10.0.10240
+ !insertmacro __MOZ__WinVer_DefineOSTests 10
+!endif
+
+!verbose push
+!verbose 3
+!ifndef _OVERRIDE_VERBOSE
+ !define _OVERRIDE_VERBOSE 3
+!endif
+!verbose ${_OVERRIDE_VERBOSE}
+!define OVERRIDE_VERBOSE `!insertmacro OVERRIDE_VERBOSE`
+!define _OVERRIDE_UN
+!define _OVERRIDE_S
+!verbose pop
+
+!macro OVERRIDE_VERBOSE _VERBOSE
+ !verbose push
+ !verbose 3
+ !undef _OVERRIDE_VERBOSE
+ !define _OVERRIDE_VERBOSE ${_VERBOSE}
+ !verbose pop
+!macroend
+
+; Modified version of Locate from the NSIS File Functions Header v3.4 (it has
+; the same version in earlier versions of NSIS even though it has changed) that
+; is distributed with NSIS v2.46-Unicode. This version has the calls to
+; SetDetailsPrint commented out.
+; See <NSIS v2.46-Unicode App Dir>/include/FileFunc.nsh for more information.
+!macro LocateNoDetailsCall _PATH _OPTIONS _FUNC
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ Push $0
+ Push `${_PATH}`
+ Push `${_OPTIONS}`
+ GetFunctionAddress $0 `${_FUNC}`
+ Push `$0`
+ Call LocateNoDetails
+ Pop $0
+ !verbose pop
+!macroend
+
+!macro LocateNoDetails
+ !ifndef ${_OVERRIDE_UN}LocateNoDetails
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ !define ${_OVERRIDE_UN}LocateNoDetails `!insertmacro ${_OVERRIDE_UN}LocateNoDetailsCall`
+
+ Function ${_OVERRIDE_UN}LocateNoDetails
+ Exch $2
+ Exch
+ Exch $1
+ Exch
+ Exch 2
+ Exch $0
+ Exch 2
+ Push $3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+ Push $8
+ Push $9
+ Push $R6
+ Push $R7
+ Push $R8
+ Push $R9
+ ClearErrors
+
+ StrCpy $3 ''
+ StrCpy $4 ''
+ StrCpy $5 ''
+ StrCpy $6 ''
+ StrCpy $7 ''
+ StrCpy $8 0
+ StrCpy $R7 ''
+
+ StrCpy $R9 $0 1 -1
+ StrCmp $R9 '\' 0 +3
+ StrCpy $0 $0 -1
+ goto -3
+ IfFileExists '$0\*.*' 0 error
+
+ option:
+ StrCpy $R9 $1 1
+ StrCpy $1 $1 '' 1
+ StrCmp $R9 ' ' -2
+ StrCmp $R9 '' sizeset
+ StrCmp $R9 '/' 0 -4
+ StrCpy $9 -1
+ IntOp $9 $9 + 1
+ StrCpy $R9 $1 1 $9
+ StrCmp $R9 '' +2
+ StrCmp $R9 '/' 0 -3
+ StrCpy $R8 $1 $9
+ StrCpy $R8 $R8 '' 2
+ StrCpy $R9 $R8 '' -1
+ StrCmp $R9 ' ' 0 +3
+ StrCpy $R8 $R8 -1
+ goto -3
+ StrCpy $R9 $1 2
+ StrCpy $1 $1 '' $9
+
+ StrCmp $R9 'L=' 0 mask
+ StrCpy $3 $R8
+ StrCmp $3 '' +6
+ StrCmp $3 'FD' +5
+ StrCmp $3 'F' +4
+ StrCmp $3 'D' +3
+ StrCmp $3 'DE' +2
+ StrCmp $3 'FDE' 0 error
+ goto option
+
+ mask:
+ StrCmp $R9 'M=' 0 size
+ StrCpy $4 $R8
+ goto option
+
+ size:
+ StrCmp $R9 'S=' 0 gotosubdir
+ StrCpy $6 $R8
+ goto option
+
+ gotosubdir:
+ StrCmp $R9 'G=' 0 banner
+ StrCpy $7 $R8
+ StrCmp $7 '' +3
+ StrCmp $7 '1' +2
+ StrCmp $7 '0' 0 error
+ goto option
+
+ banner:
+ StrCmp $R9 'B=' 0 error
+ StrCpy $R7 $R8
+ StrCmp $R7 '' +3
+ StrCmp $R7 '1' +2
+ StrCmp $R7 '0' 0 error
+ goto option
+
+ sizeset:
+ StrCmp $6 '' default
+ StrCpy $9 0
+ StrCpy $R9 $6 1 $9
+ StrCmp $R9 '' +4
+ StrCmp $R9 ':' +3
+ IntOp $9 $9 + 1
+ goto -4
+ StrCpy $5 $6 $9
+ IntOp $9 $9 + 1
+ StrCpy $1 $6 1 -1
+ StrCpy $6 $6 -1 $9
+ StrCmp $5 '' +2
+ IntOp $5 $5 + 0
+ StrCmp $6 '' +2
+ IntOp $6 $6 + 0
+
+ StrCmp $1 'B' 0 +3
+ StrCpy $1 1
+ goto default
+ StrCmp $1 'K' 0 +3
+ StrCpy $1 1024
+ goto default
+ StrCmp $1 'M' 0 +3
+ StrCpy $1 1048576
+ goto default
+ StrCmp $1 'G' 0 error
+ StrCpy $1 1073741824
+
+ default:
+ StrCmp $3 '' 0 +2
+ StrCpy $3 'FD'
+ StrCmp $4 '' 0 +2
+ StrCpy $4 '*.*'
+ StrCmp $7 '' 0 +2
+ StrCpy $7 '1'
+ StrCmp $R7 '' 0 +2
+ StrCpy $R7 '0'
+ StrCpy $7 'G$7B$R7'
+
+ StrCpy $8 1
+ Push $0
+; SetDetailsPrint textonly
+
+ nextdir:
+ IntOp $8 $8 - 1
+ Pop $R8
+
+ StrCpy $9 $7 2 2
+ StrCmp $9 'B0' +3
+ GetLabelAddress $9 findfirst
+ goto call
+; DetailPrint 'Search in: $R8'
+
+ findfirst:
+ FindFirst $0 $R7 '$R8\$4'
+ IfErrors subdir
+ StrCmp $R7 '.' 0 dir
+ FindNext $0 $R7
+ StrCmp $R7 '..' 0 dir
+ FindNext $0 $R7
+ IfErrors 0 dir
+ FindClose $0
+ goto subdir
+
+ dir:
+ IfFileExists '$R8\$R7\*.*' 0 file
+ StrCpy $R6 ''
+ StrCmp $3 'DE' +4
+ StrCmp $3 'FDE' +3
+ StrCmp $3 'FD' precall
+ StrCmp $3 'F' findnext precall
+ FindFirst $9 $R9 '$R8\$R7\*.*'
+ StrCmp $R9 '.' 0 +4
+ FindNext $9 $R9
+ StrCmp $R9 '..' 0 +2
+ FindNext $9 $R9
+ FindClose $9
+ IfErrors precall findnext
+
+ file:
+ StrCmp $3 'FDE' +3
+ StrCmp $3 'FD' +2
+ StrCmp $3 'F' 0 findnext
+ StrCpy $R6 0
+ StrCmp $5$6 '' precall
+ FileOpen $9 '$R8\$R7' r
+ IfErrors +3
+ FileSeek $9 0 END $R6
+ FileClose $9
+ System::Int64Op $R6 / $1
+ Pop $R6
+ StrCmp $5 '' +2
+ IntCmp $R6 $5 0 findnext
+ StrCmp $6 '' +2
+ IntCmp $R6 $6 0 0 findnext
+
+ precall:
+ StrCpy $9 0
+ StrCpy $R9 '$R8\$R7'
+
+ call:
+ Push $0
+ Push $1
+ Push $2
+ Push $3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+ Push $8
+ Push $9
+ Push $R7
+ Push $R8
+ StrCmp $9 0 +4
+ StrCpy $R6 ''
+ StrCpy $R7 ''
+ StrCpy $R9 ''
+ Call $2
+ Pop $R9
+ Pop $R8
+ Pop $R7
+ Pop $9
+ Pop $8
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Pop $2
+ Pop $1
+ Pop $0
+
+ IfErrors 0 +3
+ FindClose $0
+ goto error
+ StrCmp $R9 'StopLocateNoDetails' 0 +3
+ FindClose $0
+ goto clearstack
+ goto $9
+
+ findnext:
+ FindNext $0 $R7
+ IfErrors 0 dir
+ FindClose $0
+
+ subdir:
+ StrCpy $9 $7 2
+ StrCmp $9 'G0' end
+ FindFirst $0 $R7 '$R8\*.*'
+ StrCmp $R7 '.' 0 pushdir
+ FindNext $0 $R7
+ StrCmp $R7 '..' 0 pushdir
+ FindNext $0 $R7
+ IfErrors 0 pushdir
+ FindClose $0
+ StrCmp $8 0 end nextdir
+
+ pushdir:
+ IfFileExists '$R8\$R7\*.*' 0 +3
+ Push '$R8\$R7'
+ IntOp $8 $8 + 1
+ FindNext $0 $R7
+ IfErrors 0 pushdir
+ FindClose $0
+ StrCmp $8 0 end nextdir
+
+ error:
+ SetErrors
+
+ clearstack:
+ StrCmp $8 0 end
+ IntOp $8 $8 - 1
+ Pop $R8
+ goto clearstack
+
+ end:
+; SetDetailsPrint both
+ Pop $R9
+ Pop $R8
+ Pop $R7
+ Pop $R6
+ Pop $9
+ Pop $8
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Pop $2
+ Pop $1
+ Pop $0
+ FunctionEnd
+
+ !verbose pop
+ !endif
+!macroend
+
+!macro un.LocateNoDetailsCall _PATH _OPTIONS _FUNC
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ Push $0
+ Push `${_PATH}`
+ Push `${_OPTIONS}`
+ GetFunctionAddress $0 `${_FUNC}`
+ Push `$0`
+ Call un.LocateNoDetails
+ Pop $0
+ !verbose pop
+!macroend
+
+!macro un.LocateNoDetails
+ !ifndef un.LocateNoDetails
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ !undef _OVERRIDE_UN
+ !define _OVERRIDE_UN `un.`
+
+ !insertmacro LocateNoDetails
+
+ !undef _OVERRIDE_UN
+ !define _OVERRIDE_UN
+ !verbose pop
+ !endif
+!macroend
+
+; Modified version of TextCompare from the NSIS Text Functions Header v2.4 (it
+; has the same version in earlier versions of NSIS even though it has changed)
+; that is distributed with NSIS v2.46-Unicode. This version has the calls to
+; SetDetailsPrint commented out.
+; See <NSIS v2.46-Unicode App Dir>/include/TextFunc.nsh for more information.
+!macro TextCompareNoDetailsCall _FILE1 _FILE2 _OPTION _FUNC
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ Push $0
+ Push `${_FILE1}`
+ Push `${_FILE2}`
+ Push `${_OPTION}`
+ GetFunctionAddress $0 `${_FUNC}`
+ Push `$0`
+ ${CallArtificialFunction} TextCompareNoDetails_
+ Pop $0
+ !verbose pop
+!macroend
+
+!macro TextCompareNoDetailsSCall _FILE1 _FILE2 _OPTION _FUNC
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+ Push $0
+ Push `${_FILE1}`
+ Push `${_FILE2}`
+ Push `${_OPTION}`
+ GetFunctionAddress $0 `${_FUNC}`
+ Push `$0`
+ ${CallArtificialFunction} TextCompareNoDetailsS_
+ Pop $0
+ !verbose pop
+!macroend
+
+
+!macro TextCompareNoDetailsBody _OVERRIDE_S
+ Exch $3
+ Exch
+ Exch $2
+ Exch
+ Exch 2
+ Exch $1
+ Exch 2
+ Exch 3
+ Exch $0
+ Exch 3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+ Push $8
+ Push $9
+ ClearErrors
+
+ IfFileExists $0 0 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error
+ IfFileExists $1 0 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error
+ StrCmp $2 'FastDiff' +5
+ StrCmp $2 'FastEqual' +4
+ StrCmp $2 'SlowDiff' +3
+ StrCmp $2 'SlowEqual' +2
+ goto TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error
+
+ FileOpen $4 $0 r
+ IfErrors TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error
+ FileOpen $5 $1 r
+ IfErrors TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error
+; SetDetailsPrint textonly
+
+ StrCpy $6 0
+ StrCpy $8 0
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline:
+ StrCmp${_OVERRIDE_S} $4 '' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_fast
+ IntOp $8 $8 + 1
+ FileRead $4 $9
+ IfErrors 0 +4
+ FileClose $4
+ StrCpy $4 ''
+ StrCmp${_OVERRIDE_S} $5 '' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_end
+ StrCmp $2 'FastDiff' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_fast
+ StrCmp $2 'FastEqual' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_fast TextFunc_TextCompareNoDetails${_OVERRIDE_S}_slow
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_fast:
+ StrCmp${_OVERRIDE_S} $5 '' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call
+ IntOp $6 $6 + 1
+ FileRead $5 $7
+ IfErrors 0 +5
+ FileClose $5
+ StrCpy $5 ''
+ StrCmp${_OVERRIDE_S} $4 '' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_end
+ StrCmp $2 'FastDiff' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call TextFunc_TextCompareNoDetails${_OVERRIDE_S}_close
+ StrCmp $2 'FastDiff' 0 +2
+ StrCmp${_OVERRIDE_S} $7 $9 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call
+ StrCmp${_OVERRIDE_S} $7 $9 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_slow:
+ StrCmp${_OVERRIDE_S} $4 '' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_close
+ StrCpy $6 ''
+; DetailPrint '$8. $9'
+ FileSeek $5 0
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_slownext:
+ FileRead $5 $7
+ IfErrors 0 +2
+ StrCmp $2 'SlowDiff' TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline
+ StrCmp $2 'SlowDiff' 0 +2
+ StrCmp${_OVERRIDE_S} $7 $9 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline TextFunc_TextCompareNoDetails${_OVERRIDE_S}_slownext
+ IntOp $6 $6 + 1
+ StrCmp${_OVERRIDE_S} $7 $9 0 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_slownext
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_call:
+ Push $2
+ Push $3
+ Push $4
+ Push $5
+ Push $6
+ Push $7
+ Push $8
+ Push $9
+ Call $3
+ Pop $0
+ Pop $9
+ Pop $8
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Pop $2
+ StrCmp $0 'StopTextCompareNoDetails' 0 TextFunc_TextCompareNoDetails${_OVERRIDE_S}_nextline
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_close:
+ FileClose $4
+ FileClose $5
+ goto TextFunc_TextCompareNoDetails${_OVERRIDE_S}_end
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_error:
+ SetErrors
+
+ TextFunc_TextCompareNoDetails${_OVERRIDE_S}_end:
+; SetDetailsPrint both
+ Pop $9
+ Pop $8
+ Pop $7
+ Pop $6
+ Pop $5
+ Pop $4
+ Pop $3
+ Pop $2
+ Pop $1
+ Pop $0
+!macroend
+
+!define TextCompareNoDetails `!insertmacro TextCompareNoDetailsCall`
+!define un.TextCompareNoDetails `!insertmacro TextCompareNoDetailsCall`
+
+!macro TextCompareNoDetails
+!macroend
+
+!macro un.TextCompareNoDetails
+!macroend
+
+!macro TextCompareNoDetails_
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+
+ !insertmacro TextCompareNoDetailsBody ''
+
+ !verbose pop
+!macroend
+
+!define TextCompareNoDetailsS `!insertmacro TextCompareNoDetailsSCall`
+!define un.TextCompareNoDetailsS `!insertmacro TextCompareNoDetailsSCall`
+
+!macro TextCompareNoDetailsS
+!macroend
+
+!macro un.TextCompareNoDetailsS
+!macroend
+
+!macro TextCompareNoDetailsS_
+ !verbose push
+ !verbose ${_OVERRIDE_VERBOSE}
+
+ !insertmacro TextCompareNoDetailsBody 'S'
+
+ !verbose pop
+!macroend
+
+!endif
diff --git a/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py b/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py
new file mode 100644
index 000000000..cb7303a12
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/preprocess-locale.py
@@ -0,0 +1,360 @@
+# preprocess-locale.py
+# Any copyright is dedicated to the Public Domain.
+# http://creativecommons.org/publicdomain/zero/1.0/
+
+# preprocess-locale.py provides two functions depending on the arguments passed
+# to it when invoked.
+#
+# Preprocesses installer locale properties files and creates a basic NSIS nlf
+# file when invoked with --preprocess-locale.
+#
+# Converts a UTF-8 file to a new UTF-16LE file when invoked with
+# --convert-utf8-utf16le.
+
+from codecs import BOM_UTF16_LE
+from os.path import join, isfile
+import sys
+from optparse import OptionParser
+
+def open_utf16le_file(path):
+ """
+ Returns an opened file object with a a UTF-16LE byte order mark.
+ """
+ fp = open(path, "w+b")
+ fp.write(BOM_UTF16_LE)
+ return fp
+
+def get_locale_strings(path, prefix, middle, add_cr):
+ """
+ Returns a string created by converting an installer locale properties file
+ into the format required by NSIS locale files.
+
+ Parameters:
+ path - the path to the installer locale properties file to preprocess
+ prefix - a string to prefix each line with
+ middle - a string to insert between the name and value for each line
+ add_cr - boolean for whether to add an NSIS carriage return before NSIS
+ linefeeds when there isn't one already
+ """
+ output = ""
+ fp = open(path, "r")
+ for line in fp:
+ line = line.strip()
+ if line == "" or line[0] == "#":
+ continue
+
+ name, value = line.split("=", 1)
+ value = value.strip() # trim whitespace from the start and end
+ if value and value[-1] == "\"" and value[0] == "\"":
+ value = value[1:-1] # remove " from the start and end
+
+ if add_cr:
+ value = value.replace("\\n", "\\r\\n") # prefix $\n with $\r
+ value = value.replace("\\r\\r", "\\r") # replace $\r$\r with $\r
+
+ value = value.replace("\"", "$\\\"") # prefix " with $\
+ value = value.replace("\\r", "$\\r") # prefix \r with $
+ value = value.replace("\\n", "$\\n") # prefix \n with $
+ value = value.replace("\\t", "$\\t") # prefix \t with $
+
+ output += prefix + name.strip() + middle + " \"" + value + "\"\n"
+ fp.close()
+ return output
+
+def lookup(path, l10ndirs):
+ for d in l10ndirs:
+ if isfile(join(d, path)):
+ return join(d, path)
+ return join(l10ndirs[-1], path)
+
+def preprocess_locale_files(config_dir, l10ndirs):
+ """
+ Preprocesses the installer localized properties files into the format
+ required by NSIS and creates a basic NSIS nlf file.
+
+ Parameters:
+ config_dir - the path to the destination directory
+ l10ndirs - list of paths to search for installer locale files
+ """
+
+ # Create the main NSIS language file
+ fp = open_utf16le_file(join(config_dir, "overrideLocale.nsh"))
+ locale_strings = get_locale_strings(lookup("override.properties",
+ l10ndirs),
+ "LangString ^",
+ " 0 ",
+ False)
+ fp.write(unicode(locale_strings, "utf-8").encode("utf-16-le"))
+ fp.close()
+
+ # Create the Modern User Interface language file
+ fp = open_utf16le_file(join(config_dir, "baseLocale.nsh"))
+ fp.write((u""";NSIS Modern User Interface - Language File
+;Compatible with Modern UI 1.68
+;Language: baseLocale (0)
+!insertmacro MOZ_MUI_LANGUAGEFILE_BEGIN \"baseLocale\"
+!define MUI_LANGNAME \"baseLocale\"
+""").encode("utf-16-le"))
+ locale_strings = get_locale_strings(lookup("mui.properties", l10ndirs),
+ "!define ", " ", True)
+ fp.write(unicode(locale_strings, "utf-8").encode("utf-16-le"))
+ fp.write(u"!insertmacro MOZ_MUI_LANGUAGEFILE_END\n".encode("utf-16-le"))
+ fp.close()
+
+ # Create the custom language file for our custom strings
+ fp = open_utf16le_file(join(config_dir, "customLocale.nsh"))
+ locale_strings = get_locale_strings(lookup("custom.properties",
+ l10ndirs),
+ "LangString ",
+ " 0 ",
+ True)
+ fp.write(unicode(locale_strings, "utf-8").encode("utf-16-le"))
+ fp.close()
+
+def create_nlf_file(moz_dir, ab_cd, config_dir):
+ """
+ Create a basic NSIS nlf file.
+
+ Parameters:
+ moz_dir - the path to top source directory for the toolkit source
+ ab_cd - the locale code
+ config_dir - the path to the destination directory
+ """
+ rtl = "-"
+
+ # Check whether the locale is right to left from locales.nsi.
+ fp = open(join(moz_dir,
+ "toolkit/mozapps/installer/windows/nsis/locales.nsi"),
+ "r")
+ for line in fp:
+ line = line.strip()
+ if line == "!define " + ab_cd + "_rtl":
+ rtl = "RTL"
+ break
+
+ fp.close()
+
+ # Create the main NSIS language file with RTL for right to left locales
+ # along with the default codepage, font name, and font size represented
+ # by the '-' character.
+ fp = open_utf16le_file(join(config_dir, "baseLocale.nlf"))
+ fp.write((u"""# Header, don't edit
+NLF v6
+# Start editing here
+# Language ID
+0
+# Font and size - dash (-) means default
+-
+-
+# Codepage - dash (-) means ANSI code page
+-
+# RTL - anything else than RTL means LTR
+%s
+""" % rtl).encode("utf-16-le"))
+ fp.close()
+
+def preprocess_locale_file(config_dir,
+ l10ndirs,
+ properties_filename,
+ output_filename):
+ """
+ Preprocesses a single localized properties file into the format
+ required by NSIS and creates a basic NSIS nlf file.
+
+ Parameters:
+ config_dir - the path to the destination directory
+ l10ndirs - list of paths to search for installer locale files
+ properties_filename - the name of the properties file to search for
+ output_filename - the output filename to write
+ """
+
+ # Create the custom language file for our custom strings
+ fp = open_utf16le_file(join(config_dir, output_filename))
+ locale_strings = get_locale_strings(lookup(properties_filename,
+ l10ndirs),
+ "LangString ",
+ " 0 ",
+ True)
+ fp.write(unicode(locale_strings, "utf-8").encode("utf-16-le"))
+ fp.close()
+
+
+def convert_utf8_utf16le(in_file_path, out_file_path):
+ """
+ Converts a UTF-8 file to a new UTF-16LE file
+
+ Arguments:
+ in_file_path - the path to the UTF-8 source file to convert
+ out_file_path - the path to the UTF-16LE destination file to create
+ """
+ in_fp = open(in_file_path, "r")
+ out_fp = open_utf16le_file(out_file_path)
+ out_fp.write(unicode(in_fp.read(), "utf-8").encode("utf-16-le"))
+ in_fp.close()
+ out_fp.close()
+
+if __name__ == '__main__':
+ usage = """usage: %prog command <args>
+
+Commands:
+ --convert-utf8-utf16le - Preprocesses installer locale properties files
+ --preprocess-locale - Preprocesses the installer localized properties
+ files into the format required by NSIS and
+ creates a basic NSIS nlf file.
+ --preprocess-single-file - Preprocesses a single properties file into the
+ format required by NSIS
+ --create-nlf-file - Creates a basic NSIS nlf file
+
+preprocess-locale.py --preprocess-locale <src> <locale> <code> <dest>
+
+Arguments:
+ <src> \tthe path to top source directory for the toolkit source
+ <locale>\tthe path to the installer's locale files
+ <code> \tthe locale code
+ <dest> \tthe path to the destination directory
+
+
+preprocess-locale.py --preprocess-single-file <src>
+ <locale>
+ <dest>
+ <infile>
+ <outfile>
+
+Arguments:
+ <src> \tthe path to top source directory for the toolkit source
+ <locale> \tthe path to the installer's locale files
+ <dest> \tthe path to the destination directory
+ <infile> \tthe properties file to process
+ <outfile>\tthe nsh file to write
+
+
+preprocess-locale.py --create-nlf-file <src>
+ <code>
+ <dest>
+
+Arguments:
+ <src> \tthe path to top source directory for the toolkit source
+ <code> \tthe locale code
+ <dest> \tthe path to the destination directory
+
+
+preprocess-locale.py --convert-utf8-utf16le <src> <dest>
+
+Arguments:
+ <src> \tthe path to the UTF-8 source file to convert
+ <dest>\tthe path to the UTF-16LE destination file to create
+"""
+
+ preprocess_locale_args_help_string = """\
+Arguments to --preprocess-locale should be:
+ <src> <locale> <code> <dest>
+or
+ <src> <code> <dest> --l10n-dir <dir> [--l10n-dir <dir> ...]"""
+
+ preprocess_single_file_args_help_string = """\
+Arguments to --preprocess-single_file should be:
+ <src> <locale> <code> <dest> <infile> <outfile>
+or
+ <src> <locale> <code> <dest> <infile> <outfile>
+ --l10n-dir <dir> [--l10n-dir <dir>...]"""
+
+ create_nlf_args_help_string = """\
+Arguments to --create-nlf-file should be:
+ <src> <code> <dest>"""
+
+ p = OptionParser(usage=usage)
+ p.add_option("--preprocess-locale", action="store_true", default=False,
+ dest='preprocess')
+ p.add_option("--preprocess-single-file", action="store_true", default=False,
+ dest='preprocessSingle')
+ p.add_option("--create-nlf-file", action="store_true", default=False,
+ dest='createNlf')
+ p.add_option("--l10n-dir", action="append", default=[],
+ dest="l10n_dirs",
+ help="Add directory to lookup for locale files")
+ p.add_option("--convert-utf8-utf16le", action="store_true", default=False,
+ dest='convert')
+
+ options, args = p.parse_args()
+
+ foundOne = False
+ if (options.preprocess):
+ foundOne = True
+ if (options.convert):
+ if(foundOne):
+ p.error("More than one command specified")
+ else:
+ foundOne = True
+ if (options.preprocessSingle):
+ if(foundOne):
+ p.error("More than one command specified")
+ else:
+ foundOne = True
+ if (options.createNlf):
+ if(foundOne):
+ p.error("More than one command specified")
+ else:
+ foundOne = True
+
+ if (not foundOne):
+ p.error("No command specified")
+
+ if options.preprocess:
+ if len(args) not in (3,4):
+ p.error(preprocess_locale_args_help_string)
+
+ # Parse args
+ pargs = args[:]
+ moz_dir = pargs[0]
+ if len(pargs) == 4:
+ l10n_dirs = [pargs[1]]
+ del pargs[1]
+ else:
+ if not options.l10n_dirs:
+ p.error(preprocess_locale_args_help_string)
+ l10n_dirs = options.l10n_dirs
+ ab_cd = pargs[1]
+ config_dir = pargs[2]
+
+ # Create the output files
+ create_nlf_file(moz_dir, ab_cd, config_dir)
+ preprocess_locale_files(config_dir, l10n_dirs)
+ elif options.preprocessSingle:
+ if len(args) not in (4,5):
+ p.error(preprocess_single_file_args_help_string)
+
+ # Parse args
+ pargs = args[:]
+ moz_dir = pargs[0]
+ if len(pargs) == 5:
+ l10n_dirs = [pargs[1]]
+ del pargs[1]
+ else:
+ if not options.l10n_dirs:
+ p.error(preprocess_single_file_args_help_string)
+ l10n_dirs = options.l10n_dirs
+ config_dir = pargs[1]
+ in_file = pargs[2]
+ out_file = pargs[3]
+
+ # Create the output files
+ preprocess_locale_file(config_dir,
+ l10n_dirs,
+ in_file,
+ out_file)
+ elif options.createNlf:
+ if len(args) != 3:
+ p.error(create_nlf_args_help_string)
+
+ # Parse args
+ pargs = args[:]
+ moz_dir = pargs[0]
+ ab_cd = pargs[1]
+ config_dir = pargs[2]
+
+ # Create the output files
+ create_nlf_file(moz_dir, ab_cd, config_dir)
+ elif options.convert:
+ if len(args) != 2:
+ p.error("--convert-utf8-utf16le needs both of <src> <dest>")
+ convert_utf8_utf16le(*args)
diff --git a/toolkit/mozapps/installer/windows/nsis/setup.ico b/toolkit/mozapps/installer/windows/nsis/setup.ico
new file mode 100644
index 000000000..9801fed54
--- /dev/null
+++ b/toolkit/mozapps/installer/windows/nsis/setup.ico
Binary files differ
diff --git a/toolkit/mozapps/preferences/changemp.js b/toolkit/mozapps/preferences/changemp.js
new file mode 100644
index 000000000..82dd20128
--- /dev/null
+++ b/toolkit/mozapps/preferences/changemp.js
@@ -0,0 +1,237 @@
+// -*- tab-width: 2; 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/. */
+
+const nsPK11TokenDB = "@mozilla.org/security/pk11tokendb;1";
+const nsIPK11TokenDB = Components.interfaces.nsIPK11TokenDB;
+const nsIDialogParamBlock = Components.interfaces.nsIDialogParamBlock;
+const nsPKCS11ModuleDB = "@mozilla.org/security/pkcs11moduledb;1";
+const nsIPKCS11ModuleDB = Components.interfaces.nsIPKCS11ModuleDB;
+const nsIPKCS11Slot = Components.interfaces.nsIPKCS11Slot;
+const nsIPK11Token = Components.interfaces.nsIPK11Token;
+
+
+var params;
+var tokenName="";
+var pw1;
+
+function init()
+{
+ pw1 = document.getElementById("pw1");
+
+ process();
+}
+
+
+function process()
+{
+ var secmoddb = Components.classes[nsPKCS11ModuleDB].getService(nsIPKCS11ModuleDB);
+ var bundle = document.getElementById("bundlePreferences");
+
+ // If the token is unitialized, don't use the old password box.
+ // Otherwise, do.
+
+ var slot = secmoddb.findSlotByName(tokenName);
+ if (slot) {
+ var oldpwbox = document.getElementById("oldpw");
+ var msgBox = document.getElementById("message");
+ var status = slot.status;
+ if (status == nsIPKCS11Slot.SLOT_UNINITIALIZED
+ || status == nsIPKCS11Slot.SLOT_READY) {
+
+ oldpwbox.setAttribute("hidden", "true");
+ msgBox.setAttribute("value", bundle.getString("password_not_set"));
+ msgBox.setAttribute("hidden", "false");
+
+ if (status == nsIPKCS11Slot.SLOT_READY) {
+ oldpwbox.setAttribute("inited", "empty");
+ } else {
+ oldpwbox.setAttribute("inited", "true");
+ }
+
+ // Select first password field
+ document.getElementById('pw1').focus();
+
+ } else {
+ // Select old password field
+ oldpwbox.setAttribute("hidden", "false");
+ msgBox.setAttribute("hidden", "true");
+ oldpwbox.setAttribute("inited", "false");
+ oldpwbox.focus();
+ }
+ }
+
+ if (params) {
+ // Return value 0 means "canceled"
+ params.SetInt(1, 0);
+ }
+
+ checkPasswords();
+}
+
+function setPassword()
+{
+ var pk11db = Components.classes[nsPK11TokenDB].getService(nsIPK11TokenDB);
+ var promptService = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ var token = pk11db.findTokenByName(tokenName);
+ dump("*** TOKEN!!!! (name = |" + token + "|\n");
+
+ var oldpwbox = document.getElementById("oldpw");
+ var initpw = oldpwbox.getAttribute("inited");
+ var bundle = document.getElementById("bundlePreferences");
+
+ var success = false;
+
+ if (initpw == "false" || initpw == "empty") {
+ try {
+ var oldpw = "";
+ var passok = 0;
+
+ if (initpw == "empty") {
+ passok = 1;
+ } else {
+ oldpw = oldpwbox.value;
+ passok = token.checkPassword(oldpw);
+ }
+
+ if (passok) {
+ if (initpw == "empty" && pw1.value == "") {
+ // This makes no sense that we arrive here,
+ // we reached a case that should have been prevented by checkPasswords.
+ } else {
+ if (pw1.value == "") {
+ var secmoddb = Components.classes[nsPKCS11ModuleDB].getService(nsIPKCS11ModuleDB);
+ if (secmoddb.isFIPSEnabled) {
+ // empty passwords are not allowed in FIPS mode
+ promptService.alert(window,
+ bundle.getString("pw_change_failed_title"),
+ bundle.getString("pw_change2empty_in_fips_mode"));
+ passok = 0;
+ }
+ }
+ if (passok) {
+ token.changePassword(oldpw, pw1.value);
+ if (pw1.value == "") {
+ promptService.alert(window,
+ bundle.getString("pw_change_success_title"),
+ bundle.getString("pw_erased_ok")
+ + " " + bundle.getString("pw_empty_warning"));
+ } else {
+ promptService.alert(window,
+ bundle.getString("pw_change_success_title"),
+ bundle.getString("pw_change_ok"));
+ }
+ success = true;
+ }
+ }
+ } else {
+ oldpwbox.focus();
+ oldpwbox.setAttribute("value", "");
+ promptService.alert(window,
+ bundle.getString("pw_change_failed_title"),
+ bundle.getString("incorrect_pw"));
+ }
+ } catch (e) {
+ promptService.alert(window,
+ bundle.getString("pw_change_failed_title"),
+ bundle.getString("failed_pw_change"));
+ }
+ } else {
+ token.initPassword(pw1.value);
+ if (pw1.value == "") {
+ promptService.alert(window,
+ bundle.getString("pw_change_success_title"),
+ bundle.getString("pw_not_wanted")
+ + " " + bundle.getString("pw_empty_warning"));
+ }
+ success = true;
+ }
+
+ // Terminate dialog
+ if (success)
+ window.close();
+}
+
+function setPasswordStrength()
+{
+// Here is how we weigh the quality of the password
+// number of characters
+// numbers
+// non-alpha-numeric chars
+// upper and lower case characters
+
+ var pw=document.getElementById('pw1').value;
+
+// length of the password
+ var pwlength=(pw.length);
+ if (pwlength>5)
+ pwlength=5;
+
+
+// use of numbers in the password
+ var numnumeric = pw.replace (/[0-9]/g, "");
+ var numeric=(pw.length - numnumeric.length);
+ if (numeric>3)
+ numeric=3;
+
+// use of symbols in the password
+ var symbols = pw.replace (/\W/g, "");
+ var numsymbols=(pw.length - symbols.length);
+ if (numsymbols>3)
+ numsymbols=3;
+
+// use of uppercase in the password
+ var numupper = pw.replace (/[A-Z]/g, "");
+ var upper=(pw.length - numupper.length);
+ if (upper>3)
+ upper=3;
+
+
+ var pwstrength=((pwlength*10)-20) + (numeric*10) + (numsymbols*15) + (upper*10);
+
+ // make sure we're give a value between 0 and 100
+ if ( pwstrength < 0 ) {
+ pwstrength = 0;
+ }
+
+ if ( pwstrength > 100 ) {
+ pwstrength = 100;
+ }
+
+ var mymeter=document.getElementById('pwmeter');
+ mymeter.value = pwstrength;
+
+ return;
+}
+
+function checkPasswords()
+{
+ var pw1=document.getElementById('pw1').value;
+ var pw2=document.getElementById('pw2').value;
+ var ok=document.documentElement.getButton("accept");
+
+ var oldpwbox = document.getElementById("oldpw");
+ if (oldpwbox) {
+ var initpw = oldpwbox.getAttribute("inited");
+
+ if (initpw == "empty" && pw1 == "") {
+ // The token has already been initialized, therefore this dialog
+ // was called with the intention to change the password.
+ // The token currently uses an empty password.
+ // We will not allow changing the password from empty to empty.
+ ok.setAttribute("disabled", "true");
+ return;
+ }
+ }
+
+ if (pw1 == pw2) {
+ ok.setAttribute("disabled", "false");
+ } else
+ {
+ ok.setAttribute("disabled", "true");
+ }
+
+}
diff --git a/toolkit/mozapps/preferences/changemp.xul b/toolkit/mozapps/preferences/changemp.xul
new file mode 100644
index 000000000..14d02295e
--- /dev/null
+++ b/toolkit/mozapps/preferences/changemp.xul
@@ -0,0 +1,67 @@
+<?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"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+<!ENTITY % changempDTD SYSTEM "chrome://mozapps/locale/preferences/changemp.dtd" >
+%brandDTD;
+%changempDTD;
+]>
+
+<dialog id="changemp" title="&setPassword.title;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="width: 40em;"
+ ondialogaccept="setPassword();"
+ onload="init()">
+
+ <script type="application/javascript" src="chrome://mozapps/content/preferences/changemp.js"/>
+
+ <stringbundle id="bundlePreferences" src="chrome://mozapps/locale/preferences/preferences.properties"/>
+
+ <description control="pw1">&masterPasswordDescription.label;</description>
+
+ <groupbox>
+ <grid>
+ <columns>
+ <column flex="1"/>
+ <column/>
+ </columns>
+ <rows>
+ <row>
+ <label control="oldpw">&setPassword.oldPassword.label;</label>
+ <textbox id="oldpw" type="password"/>
+ <!-- This textbox is inserted as a workaround to the fact that making the 'type'
+ & 'disabled' property of the 'oldpw' textbox toggle between ['password' &
+ 'false'] and ['text' & 'true'] - as would be necessary if the menu has more
+ than one tokens, some initialized and some not - does not work properly. So,
+ either the textbox 'oldpw' or the textbox 'message' would be displayed,
+ depending on the state of the token selected
+ -->
+ <textbox id="message" disabled="true" />
+ </row>
+ <row>
+ <label control="pw1">&setPassword.newPassword.label;</label>
+ <textbox id="pw1" type="password"
+ oninput="setPasswordStrength(); checkPasswords();"/>
+ </row>
+ <row>
+ <label control="pw2">&setPassword.reenterPassword.label;</label>
+ <textbox id="pw2" type="password" oninput="checkPasswords();"/>
+ </row>
+ </rows>
+ </grid>
+ </groupbox>
+
+ <groupbox>
+ <caption label="&setPassword.meter.label;"/>
+ <progressmeter id="pwmeter" mode="determined" value="0"/>
+ </groupbox>
+
+ <description control="pw2" class="header">&masterPasswordWarning.label;</description>
+
+</dialog>
diff --git a/toolkit/mozapps/preferences/fontbuilder.js b/toolkit/mozapps/preferences/fontbuilder.js
new file mode 100644
index 000000000..a76ce6b25
--- /dev/null
+++ b/toolkit/mozapps/preferences/fontbuilder.js
@@ -0,0 +1,126 @@
+// -*- 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/. */
+
+var FontBuilder = {
+ _enumerator: null,
+ get enumerator ()
+ {
+ if (!this._enumerator) {
+ this._enumerator = Components.classes["@mozilla.org/gfx/fontenumerator;1"]
+ .createInstance(Components.interfaces.nsIFontEnumerator);
+ }
+ return this._enumerator;
+ },
+
+ _allFonts: null,
+ _langGroupSupported: false,
+ buildFontList: function (aLanguage, aFontType, aMenuList)
+ {
+ // Reset the list
+ while (aMenuList.hasChildNodes())
+ aMenuList.removeChild(aMenuList.firstChild);
+
+ var defaultFont = null;
+ // Load Font Lists
+ var fonts = this.enumerator.EnumerateFonts(aLanguage, aFontType, { } );
+ if (fonts.length > 0)
+ defaultFont = this.enumerator.getDefaultFont(aLanguage, aFontType);
+ else {
+ fonts = this.enumerator.EnumerateFonts(aLanguage, "", { });
+ if (fonts.length > 0)
+ defaultFont = this.enumerator.getDefaultFont(aLanguage, "");
+ }
+
+ if (!this._allFonts)
+ this._allFonts = this.enumerator.EnumerateAllFonts({});
+
+ // Build the UI for the Default Font and Fonts for this CSS type.
+ var popup = document.createElement("menupopup");
+ var separator;
+ if (fonts.length > 0) {
+ if (defaultFont) {
+ var bundlePreferences = document.getElementById("bundlePreferences");
+ var label = bundlePreferences.getFormattedString("labelDefaultFont", [defaultFont]);
+ var menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("label", label);
+ menuitem.setAttribute("value", ""); // Default Font has a blank value
+ popup.appendChild(menuitem);
+
+ separator = document.createElement("menuseparator");
+ popup.appendChild(separator);
+ }
+
+ for (var i = 0; i < fonts.length; ++i) {
+ menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("value", fonts[i]);
+ menuitem.setAttribute("label", fonts[i]);
+ popup.appendChild(menuitem);
+ }
+ }
+
+ // Build the UI for the remaining fonts.
+ if (this._allFonts.length > fonts.length) {
+ this._langGroupSupported = true;
+ // Both lists are sorted, and the Fonts-By-Type list is a subset of the
+ // All-Fonts list, so walk both lists side-by-side, skipping values we've
+ // already created menu items for.
+ var builtItem = separator ? separator.nextSibling : popup.firstChild;
+ var builtItemValue = builtItem ? builtItem.getAttribute("value") : null;
+
+ separator = document.createElement("menuseparator");
+ popup.appendChild(separator);
+
+ for (i = 0; i < this._allFonts.length; ++i) {
+ if (this._allFonts[i] != builtItemValue) {
+ menuitem = document.createElement("menuitem");
+ menuitem.setAttribute("value", this._allFonts[i]);
+ menuitem.setAttribute("label", this._allFonts[i]);
+ popup.appendChild(menuitem);
+ }
+ else {
+ builtItem = builtItem.nextSibling;
+ builtItemValue = builtItem ? builtItem.getAttribute("value") : null;
+ }
+ }
+ }
+ aMenuList.appendChild(popup);
+ },
+
+ readFontSelection(aElement)
+ {
+ // Determine the appropriate value to select, for the following cases:
+ // - there is no setting
+ // - the font selected by the user is no longer present (e.g. deleted from
+ // fonts folder)
+ let preference = document.getElementById(aElement.getAttribute("preference"));
+ if (preference.value) {
+ let fontItems = aElement.getElementsByAttribute("value", preference.value);
+
+ // There is a setting that actually is in the list. Respect it.
+ if (fontItems.length)
+ return undefined;
+ }
+
+ // The first item will be a reasonable choice only if the font backend
+ // supports language-specific enumaration.
+ let defaultValue = this._langGroupSupported ?
+ aElement.firstChild.firstChild.getAttribute("value") : "";
+ let fontNameList = preference.name.replace(".name.", ".name-list.");
+ let prefFontNameList = document.getElementById(fontNameList);
+ if (!prefFontNameList || !prefFontNameList.value)
+ return defaultValue;
+
+ let fontNames = prefFontNameList.value.split(",");
+
+ for (let i = 0; i < fontNames.length; ++i) {
+ let fontName = this.enumerator.getStandardFamilyName(fontNames[i].trim());
+ let fontItems = aElement.getElementsByAttribute("value", fontName);
+ if (fontItems.length)
+ return fontItems[0].getAttribute("value");
+ }
+ return defaultValue;
+ }
+};
diff --git a/toolkit/mozapps/preferences/jar.mn b/toolkit/mozapps/preferences/jar.mn
new file mode 100644
index 000000000..3ebdff5da
--- /dev/null
+++ b/toolkit/mozapps/preferences/jar.mn
@@ -0,0 +1,11 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+ content/mozapps/preferences/fontbuilder.js
+ content/mozapps/preferences/changemp.js
+ content/mozapps/preferences/changemp.xul
+ content/mozapps/preferences/removemp.js
+ content/mozapps/preferences/removemp.xul
diff --git a/toolkit/mozapps/preferences/moz.build b/toolkit/mozapps/preferences/moz.build
new file mode 100644
index 000000000..eb4454d28
--- /dev/null
+++ b/toolkit/mozapps/preferences/moz.build
@@ -0,0 +1,7 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+JAR_MANIFESTS += ['jar.mn'] \ No newline at end of file
diff --git a/toolkit/mozapps/preferences/removemp.js b/toolkit/mozapps/preferences/removemp.js
new file mode 100644
index 000000000..1f6356eac
--- /dev/null
+++ b/toolkit/mozapps/preferences/removemp.js
@@ -0,0 +1,56 @@
+// -*- 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/. */
+
+var gRemovePasswordDialog = {
+ _token : null,
+ _bundle : null,
+ _prompt : null,
+ _okButton : null,
+ _password : null,
+ init: function ()
+ {
+ this._prompt = Components.classes["@mozilla.org/embedcomp/prompt-service;1"]
+ .getService(Components.interfaces.nsIPromptService);
+ this._bundle = document.getElementById("bundlePreferences");
+
+ this._okButton = document.documentElement.getButton("accept");
+ this._okButton.label = this._bundle.getString("pw_remove_button");
+
+ this._password = document.getElementById("password");
+
+ var pk11db = Components.classes["@mozilla.org/security/pk11tokendb;1"]
+ .getService(Components.interfaces.nsIPK11TokenDB);
+ this._token = pk11db.getInternalKeyToken();
+
+ // Initialize the enabled state of the Remove button by checking the
+ // initial value of the password ("" should be incorrect).
+ this.validateInput();
+ },
+
+ validateInput: function ()
+ {
+ this._okButton.disabled = !this._token.checkPassword(this._password.value);
+ },
+
+ removePassword: function ()
+ {
+ if (this._token.checkPassword(this._password.value)) {
+ this._token.changePassword(this._password.value, "");
+ this._prompt.alert(window,
+ this._bundle.getString("pw_change_success_title"),
+ this._bundle.getString("pw_erased_ok")
+ + " " + this._bundle.getString("pw_empty_warning"));
+ }
+ else {
+ this._password.value = "";
+ this._password.focus();
+ this._prompt.alert(window,
+ this._bundle.getString("pw_change_failed_title"),
+ this._bundle.getString("incorrect_pw"));
+ }
+ },
+};
+
diff --git a/toolkit/mozapps/preferences/removemp.xul b/toolkit/mozapps/preferences/removemp.xul
new file mode 100644
index 000000000..17ab48e39
--- /dev/null
+++ b/toolkit/mozapps/preferences/removemp.xul
@@ -0,0 +1,46 @@
+<?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"?>
+
+<!DOCTYPE dialog [
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd" >
+<!ENTITY % removempDTD SYSTEM "chrome://mozapps/locale/preferences/removemp.dtd" >
+%brandDTD;
+%removempDTD;
+]>
+
+<dialog id="removemp" title="&removePassword.title;"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="width: 35em;"
+ ondialogaccept="gRemovePasswordDialog.removePassword();"
+ onload="gRemovePasswordDialog.init()">
+
+ <script type="application/javascript" src="chrome://mozapps/content/preferences/removemp.js"/>
+
+ <stringbundle id="bundlePreferences" src="chrome://mozapps/locale/preferences/preferences.properties"/>
+
+ <vbox id="warnings">
+ <description>&removeWarning1.label;</description>
+ <description class="header">&removeWarning2.label;</description>
+ </vbox>
+
+ <separator class="thin"/>
+
+ <groupbox>
+ <caption label="&removeInfo.label;"/>
+
+ <hbox align="center">
+ <label control="password" value="&setPassword.oldPassword.label;"/>
+ <textbox id="password" type="password"
+ oninput="gRemovePasswordDialog.validateInput();"
+ aria-describedby="warnings"/>
+ </hbox>
+ </groupbox>
+
+ <separator/>
+
+</dialog>
diff --git a/toolkit/mozapps/update/UpdateTelemetry.jsm b/toolkit/mozapps/update/UpdateTelemetry.jsm
new file mode 100644
index 000000000..d64085143
--- /dev/null
+++ b/toolkit/mozapps/update/UpdateTelemetry.jsm
@@ -0,0 +1,488 @@
+/* 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 = [
+ "AUSTLMY"
+];
+
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+this.AUSTLMY = {
+ // Telemetry for the application update background update check occurs when
+ // the background update timer fires after the update interval which is
+ // determined by the app.update.interval preference and its telemetry
+ // histogram IDs have the suffix '_NOTIFY'.
+ // Telemetry for the externally initiated background update check occurs when
+ // a call is made to |checkForBackgroundUpdates| which is typically initiated
+ // by an application when it has determined that the application should have
+ // received an update. This has separate telemetry so it is possible to
+ // analyze using the telemetry data systems that have not been updating when
+ // they should have.
+
+ // The update check was performed by the call to checkForBackgroundUpdates in
+ // nsUpdateService.js.
+ EXTERNAL: "EXTERNAL",
+ // The update check was performed by the call to notify in nsUpdateService.js.
+ NOTIFY: "NOTIFY",
+
+ /**
+ * Values for the UPDATE_CHECK_CODE_NOTIFY and UPDATE_CHECK_CODE_EXTERNAL
+ * Telemetry histograms.
+ */
+ // No update found (no notification)
+ CHK_NO_UPDATE_FOUND: 0,
+ // Update will be downloaded in the background (background download)
+ CHK_DOWNLOAD_UPDATE: 1,
+ // Showing prompt due to the update.xml specifying showPrompt
+ // (update notification)
+ CHK_SHOWPROMPT_SNIPPET: 2,
+ // Showing prompt due to preference (update notification)
+ CHK_SHOWPROMPT_PREF: 3,
+ // Already has an active update in progress (no notification)
+ CHK_HAS_ACTIVEUPDATE: 8,
+ // A background download is already in progress (no notification)
+ CHK_IS_DOWNLOADING: 9,
+ // An update is already staged (no notification)
+ CHK_IS_STAGED: 10,
+ // An update is already downloaded (no notification)
+ CHK_IS_DOWNLOADED: 11,
+ // Background checks disabled by preference (no notification)
+ CHK_PREF_DISABLED: 12,
+ // Update checks disabled by admin locked preference (no notification)
+ CHK_ADMIN_DISABLED: 13,
+ // Unable to check for updates per hasUpdateMutex() (no notification)
+ CHK_NO_MUTEX: 14,
+ // Unable to check for updates per gCanCheckForUpdates (no notification). This
+ // should be covered by other codes and is recorded just in case.
+ CHK_UNABLE_TO_CHECK: 15,
+ // Background checks disabled for the current session (no notification)
+ CHK_DISABLED_FOR_SESSION: 16,
+ // Unable to perform a background check while offline (no notification)
+ CHK_OFFLINE: 17,
+ // Note: codes 18 - 21 were removed along with the certificate checking code.
+ // General update check failure and threshold reached
+ // (check failure notification)
+ CHK_GENERAL_ERROR_PROMPT: 22,
+ // General update check failure and threshold not reached (no notification)
+ CHK_GENERAL_ERROR_SILENT: 23,
+ // No compatible update found though there were updates (no notification)
+ CHK_NO_COMPAT_UPDATE_FOUND: 24,
+ // Update found for a previous version (no notification)
+ CHK_UPDATE_PREVIOUS_VERSION: 25,
+ // Update found for a version with the never preference set (no notification)
+ CHK_UPDATE_NEVER_PREF: 26,
+ // Update found without a type attribute (no notification)
+ CHK_UPDATE_INVALID_TYPE: 27,
+ // The system is no longer supported (system unsupported notification)
+ CHK_UNSUPPORTED: 28,
+ // Unable to apply updates (manual install to update notification)
+ CHK_UNABLE_TO_APPLY: 29,
+ // Unable to check for updates due to no OS version (no notification)
+ CHK_NO_OS_VERSION: 30,
+ // Unable to check for updates due to no OS ABI (no notification)
+ CHK_NO_OS_ABI: 31,
+ // Invalid url for app.update.url default preference (no notification)
+ CHK_INVALID_DEFAULT_URL: 32,
+ // Update elevation failures or cancelations threshold reached for this
+ // version, OSX only (no notification)
+ CHK_ELEVATION_DISABLED_FOR_VERSION: 35,
+ // User opted out of elevated updates for the available update version, OSX
+ // only (no notification)
+ CHK_ELEVATION_OPTOUT_FOR_VERSION: 36,
+
+ /**
+ * Submit a telemetry ping for the update check result code or a telemetry
+ * ping for a count type histogram count when no update was found. The no
+ * update found ping is separate since it is the typical result, is less
+ * interesting than the other result codes, and it is easier to analyze the
+ * other codes without including it.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_CHECK_CODE_EXTERNAL
+ * UPDATE_CHECK_CODE_NOTIFY
+ * UPDATE_CHECK_NO_UPDATE_EXTERNAL
+ * UPDATE_CHECK_NO_UPDATE_NOTIFY
+ * @param aCode
+ * An integer value as defined by the values that start with CHK_ in
+ * the above section.
+ */
+ pingCheckCode: function UT_pingCheckCode(aSuffix, aCode) {
+ try {
+ if (aCode == this.CHK_NO_UPDATE_FOUND) {
+ let id = "UPDATE_CHECK_NO_UPDATE_" + aSuffix;
+ // count type histogram
+ Services.telemetry.getHistogramById(id).add();
+ } else {
+ let id = "UPDATE_CHECK_CODE_" + aSuffix;
+ // enumerated type histogram
+ Services.telemetry.getHistogramById(id).add(aCode);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for a failed update check's unhandled error code
+ * when the pingCheckCode is CHK_GENERAL_ERROR_SILENT. The histogram is a
+ * keyed count type with key names that are prefixed with 'AUS_CHECK_EX_ERR_'.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_CHK_EXTENDED_ERROR_EXTERNAL
+ * UPDATE_CHK_EXTENDED_ERROR_NOTIFY
+ * @param aCode
+ * The extended error value return by a failed update check.
+ */
+ pingCheckExError: function UT_pingCheckExError(aSuffix, aCode) {
+ try {
+ let id = "UPDATE_CHECK_EXTENDED_ERROR_" + aSuffix;
+ let val = "AUS_CHECK_EX_ERR_" + aCode;
+ // keyed count type histogram
+ Services.telemetry.getKeyedHistogramById(id).add(val);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ // The state code and if present the status error code were read on startup.
+ STARTUP: "STARTUP",
+ // The state code and status error code if present were read after staging.
+ STAGE: "STAGE",
+
+ // Patch type Complete
+ PATCH_COMPLETE: "COMPLETE",
+ // Patch type partial
+ PATCH_PARTIAL: "PARTIAL",
+ // Patch type unknown
+ PATCH_UNKNOWN: "UNKNOWN",
+
+ /**
+ * Values for the UPDATE_DOWNLOAD_CODE_COMPLETE and
+ * UPDATE_DOWNLOAD_CODE_PARTIAL Telemetry histograms.
+ */
+ DWNLD_SUCCESS: 0,
+ DWNLD_RETRY_OFFLINE: 1,
+ DWNLD_RETRY_NET_TIMEOUT: 2,
+ DWNLD_RETRY_CONNECTION_REFUSED: 3,
+ DWNLD_RETRY_NET_RESET: 4,
+ DWNLD_ERR_NO_UPDATE: 5,
+ DWNLD_ERR_NO_UPDATE_PATCH: 6,
+ DWNLD_ERR_NO_PATCH_FILE: 7,
+ DWNLD_ERR_PATCH_SIZE_LARGER: 8,
+ DWNLD_ERR_PATCH_SIZE_NOT_EQUAL: 9,
+ DWNLD_ERR_BINDING_ABORTED: 10,
+ DWNLD_ERR_ABORT: 11,
+ DWNLD_ERR_DOCUMENT_NOT_CACHED: 12,
+ DWNLD_ERR_VERIFY_NO_REQUEST: 13,
+ DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL: 14,
+ DWNLD_ERR_VERIFY_NO_HASH_MATCH: 15,
+
+ /**
+ * Submit a telemetry ping for the update download result code.
+ *
+ * @param aIsComplete
+ * If true the histogram is for a patch type complete, if false the
+ * histogram is for a patch type partial, and when undefined the
+ * histogram is for an unknown patch type. This is used to determine
+ * the histogram ID out of the following histogram IDs:
+ * UPDATE_DOWNLOAD_CODE_COMPLETE
+ * UPDATE_DOWNLOAD_CODE_PARTIAL
+ * @param aCode
+ * An integer value as defined by the values that start with DWNLD_ in
+ * the above section.
+ */
+ pingDownloadCode: function UT_pingDownloadCode(aIsComplete, aCode) {
+ let patchType = this.PATCH_UNKNOWN;
+ if (aIsComplete === true) {
+ patchType = this.PATCH_COMPLETE;
+ } else if (aIsComplete === false) {
+ patchType = this.PATCH_PARTIAL;
+ }
+ try {
+ let id = "UPDATE_DOWNLOAD_CODE_" + patchType;
+ // enumerated type histogram
+ Services.telemetry.getHistogramById(id).add(aCode);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for the update status state code.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_STATE_CODE_COMPLETE_STARTUP
+ * UPDATE_STATE_CODE_PARTIAL_STARTUP
+ * UPDATE_STATE_CODE_UNKNOWN_STARTUP
+ * UPDATE_STATE_CODE_COMPLETE_STAGE
+ * UPDATE_STATE_CODE_PARTIAL_STAGE
+ * UPDATE_STATE_CODE_UNKNOWN_STAGE
+ * @param aCode
+ * An integer value as defined by the values that start with STATE_ in
+ * the above section for the update state from the update.status file.
+ */
+ pingStateCode: function UT_pingStateCode(aSuffix, aCode) {
+ try {
+ let id = "UPDATE_STATE_CODE_" + aSuffix;
+ // enumerated type histogram
+ Services.telemetry.getHistogramById(id).add(aCode);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for the update status error code. This does not
+ * submit a success value which can be determined from the state code.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_STATUS_ERROR_CODE_COMPLETE_STARTUP
+ * UPDATE_STATUS_ERROR_CODE_PARTIAL_STARTUP
+ * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STARTUP
+ * UPDATE_STATUS_ERROR_CODE_COMPLETE_STAGE
+ * UPDATE_STATUS_ERROR_CODE_PARTIAL_STAGE
+ * UPDATE_STATUS_ERROR_CODE_UNKNOWN_STAGE
+ * @param aCode
+ * An integer value for the error code from the update.status file.
+ */
+ pingStatusErrorCode: function UT_pingStatusErrorCode(aSuffix, aCode) {
+ try {
+ let id = "UPDATE_STATUS_ERROR_CODE_" + aSuffix;
+ // enumerated type histogram
+ Services.telemetry.getHistogramById(id).add(aCode);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit the interval in days since the last notification for this background
+ * update check or a boolean if the last notification is in the future.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_INVALID_LASTUPDATETIME_EXTERNAL
+ * UPDATE_INVALID_LASTUPDATETIME_NOTIFY
+ * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL
+ * UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY
+ */
+ pingLastUpdateTime: function UT_pingLastUpdateTime(aSuffix) {
+ const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LASTUPDATETIME)) {
+ let lastUpdateTimeSeconds = Services.prefs.getIntPref(PREF_APP_UPDATE_LASTUPDATETIME);
+ if (lastUpdateTimeSeconds) {
+ let currentTimeSeconds = Math.round(Date.now() / 1000);
+ if (lastUpdateTimeSeconds > currentTimeSeconds) {
+ try {
+ let id = "UPDATE_INVALID_LASTUPDATETIME_" + aSuffix;
+ // count type histogram
+ Services.telemetry.getHistogramById(id).add();
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ } else {
+ let intervalDays = (currentTimeSeconds - lastUpdateTimeSeconds) /
+ (60 * 60 * 24);
+ try {
+ let id = "UPDATE_LAST_NOTIFY_INTERVAL_DAYS_" + aSuffix;
+ // exponential type histogram
+ Services.telemetry.getHistogramById(id).add(intervalDays);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+ }
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for the last page displayed by the update wizard.
+ *
+ * @param aPageID
+ * The page id for the last page displayed.
+ */
+ pingWizLastPageCode: function UT_pingWizLastPageCode(aPageID) {
+ let pageMap = { invalid: 0,
+ dummy: 1,
+ checking: 2,
+ pluginupdatesfound: 3,
+ noupdatesfound: 4,
+ manualUpdate: 5,
+ unsupported: 6,
+ updatesfoundbasic: 8,
+ updatesfoundbillboard: 9,
+ downloading: 12,
+ errors: 13,
+ errorextra: 14,
+ errorpatching: 15,
+ finished: 16,
+ finishedBackground: 17,
+ installed: 18 };
+ try {
+ let id = "UPDATE_WIZ_LAST_PAGE_CODE";
+ // enumerated type histogram
+ Services.telemetry.getHistogramById(id).add(pageMap[aPageID] ||
+ pageMap.invalid);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for a boolean type histogram that indicates if the
+ * service is installed and a telemetry ping for a boolean type histogram that
+ * indicates if the service was at some point installed and is now
+ * uninstalled.
+ *
+ * @param aSuffix
+ * The histogram id suffix for histogram IDs:
+ * UPDATE_SERVICE_INSTALLED_EXTERNAL
+ * UPDATE_SERVICE_INSTALLED_NOTIFY
+ * UPDATE_SERVICE_MANUALLY_UNINSTALLED_EXTERNAL
+ * UPDATE_SERVICE_MANUALLY_UNINSTALLED_NOTIFY
+ * @param aInstalled
+ * Whether the service is installed.
+ */
+ pingServiceInstallStatus: function UT_PSIS(aSuffix, aInstalled) {
+ // Report the error but don't throw since it is more important to
+ // successfully update than to throw.
+ if (!("@mozilla.org/windows-registry-key;1" in Cc)) {
+ Cu.reportError(Cr.NS_ERROR_NOT_AVAILABLE);
+ return;
+ }
+
+ try {
+ let id = "UPDATE_SERVICE_INSTALLED_" + aSuffix;
+ // boolean type histogram
+ Services.telemetry.getHistogramById(id).add(aInstalled);
+ } catch (e) {
+ Cu.reportError(e);
+ }
+
+ let attempted = 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);
+ // Was the service at some point installed, but is now uninstalled?
+ attempted = wrk.readIntValue("Attempted");
+ wrk.close();
+ } catch (e) {
+ // Since this will throw if the registry key doesn't exist (e.g. the
+ // service has never been installed) don't report an error.
+ }
+
+ try {
+ let id = "UPDATE_SERVICE_MANUALLY_UNINSTALLED_" + aSuffix;
+ if (!aInstalled && attempted) {
+ // count type histogram
+ Services.telemetry.getHistogramById(id).add();
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for a count type histogram when the expected value
+ * does not equal the boolean value of a pref or if the pref isn't present
+ * when the expected value does not equal default value. This lessens the
+ * amount of data submitted to telemetry.
+ *
+ * @param aID
+ * The histogram ID to report to.
+ * @param aPref
+ * The preference to check.
+ * @param aDefault
+ * The default value when the preference isn't present.
+ * @param aExpected (optional)
+ * If specified and the value is the same as the value that will be
+ * added the value won't be added to telemetry.
+ */
+ pingBoolPref: function UT_pingBoolPref(aID, aPref, aDefault, aExpected) {
+ try {
+ let val = aDefault;
+ if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) {
+ val = Services.prefs.getBoolPref(aPref);
+ }
+ if (val != aExpected) {
+ // count type histogram
+ Services.telemetry.getHistogramById(aID).add();
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for a histogram with the integer value of a
+ * preference when it is not the expected value or the default value when it
+ * is not the expected value. This lessens the amount of data submitted to
+ * telemetry.
+ *
+ * @param aID
+ * The histogram ID to report to.
+ * @param aPref
+ * The preference to check.
+ * @param aDefault
+ * The default value when the pref is not set.
+ * @param aExpected (optional)
+ * If specified and the value is the same as the value that will be
+ * added the value won't be added to telemetry.
+ */
+ pingIntPref: function UT_pingIntPref(aID, aPref, aDefault, aExpected) {
+ try {
+ let val = aDefault;
+ if (Services.prefs.getPrefType(aPref) != Ci.nsIPrefBranch.PREF_INVALID) {
+ val = Services.prefs.getIntPref(aPref);
+ }
+ if (aExpected === undefined || val != aExpected) {
+ // enumerated or exponential type histogram
+ Services.telemetry.getHistogramById(aID).add(val);
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ },
+
+ /**
+ * Submit a telemetry ping for all histogram types that take a single
+ * parameter to the telemetry add function and the count type histogram when
+ * the aExpected parameter is specified. If the aExpected parameter is
+ * specified and it equals the value specified by the aValue
+ * parameter the telemetry submission will be skipped.
+ *
+ * @param aID
+ * The histogram ID to report to.
+ * @param aValue
+ * The value to add when aExpected is not defined or the value to
+ * check if it is equal to when aExpected is defined.
+ * @param aExpected (optional)
+ * If specified and the value is the same as the value specified by
+ * aValue parameter the submission will be skipped.
+ */
+ pingGeneric: function UT_pingGeneric(aID, aValue, aExpected) {
+ try {
+ if (aExpected === undefined) {
+ Services.telemetry.getHistogramById(aID).add(aValue);
+ } else if (aValue != aExpected) {
+ // count type histogram
+ Services.telemetry.getHistogramById(aID).add();
+ }
+ } catch (e) {
+ Cu.reportError(e);
+ }
+ }
+};
+Object.freeze(AUSTLMY);
diff --git a/toolkit/mozapps/update/common-standalone/moz.build b/toolkit/mozapps/update/common-standalone/moz.build
new file mode 100644
index 000000000..dbc787b43
--- /dev/null
+++ b/toolkit/mozapps/update/common-standalone/moz.build
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Library('updatecommon-standalone')
+
+srcdir = '../common'
+
+include('../common/sources.mozbuild')
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ USE_STATIC_LIBS = True
diff --git a/toolkit/mozapps/update/common/certificatecheck.cpp b/toolkit/mozapps/update/common/certificatecheck.cpp
new file mode 100644
index 000000000..afb2e7ee3
--- /dev/null
+++ b/toolkit/mozapps/update/common/certificatecheck.cpp
@@ -0,0 +1,250 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+#include <softpub.h>
+#include <wintrust.h>
+
+#include "certificatecheck.h"
+#include "updatecommon.h"
+
+static const int ENCODING = X509_ASN_ENCODING | PKCS_7_ASN_ENCODING;
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param filePath The PE file path to check
+ * @param infoToMatch The acceptable information to match
+ * @return ERROR_SUCCESS if successful, ERROR_NOT_FOUND if the info
+ * does not match, or the last error otherwise.
+ */
+DWORD
+CheckCertificateForPEFile(LPCWSTR filePath,
+ CertificateCheckInfo &infoToMatch)
+{
+ HCERTSTORE certStore = nullptr;
+ HCRYPTMSG cryptMsg = nullptr;
+ PCCERT_CONTEXT certContext = nullptr;
+ PCMSG_SIGNER_INFO signerInfo = nullptr;
+ DWORD lastError = ERROR_SUCCESS;
+
+ // Get the HCERTSTORE and HCRYPTMSG from the signed file.
+ DWORD encoding, contentType, formatType;
+ BOOL result = CryptQueryObject(CERT_QUERY_OBJECT_FILE,
+ filePath,
+ CERT_QUERY_CONTENT_FLAG_PKCS7_SIGNED_EMBED,
+ CERT_QUERY_CONTENT_FLAG_ALL,
+ 0, &encoding, &contentType,
+ &formatType, &certStore, &cryptMsg, nullptr);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptQueryObject failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Pass in nullptr to get the needed signer information size.
+ DWORD signerInfoSize;
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0,
+ nullptr, &signerInfoSize);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Allocate the needed size for the signer information.
+ signerInfo = (PCMSG_SIGNER_INFO)LocalAlloc(LPTR, signerInfoSize);
+ if (!signerInfo) {
+ lastError = GetLastError();
+ LOG_WARN(("Unable to allocate memory for Signer Info. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Get the signer information (PCMSG_SIGNER_INFO).
+ // In particular we want the issuer and serial number.
+ result = CryptMsgGetParam(cryptMsg, CMSG_SIGNER_INFO_PARAM, 0,
+ (PVOID)signerInfo, &signerInfoSize);
+ if (!result) {
+ lastError = GetLastError();
+ LOG_WARN(("CryptMsgGetParam failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ // Search for the signer certificate in the certificate store.
+ CERT_INFO certInfo;
+ certInfo.Issuer = signerInfo->Issuer;
+ certInfo.SerialNumber = signerInfo->SerialNumber;
+ certContext = CertFindCertificateInStore(certStore, ENCODING, 0,
+ CERT_FIND_SUBJECT_CERT,
+ (PVOID)&certInfo, nullptr);
+ if (!certContext) {
+ lastError = GetLastError();
+ LOG_WARN(("CertFindCertificateInStore failed. (%d)", lastError));
+ goto cleanup;
+ }
+
+ if (!DoCertificateAttributesMatch(certContext, infoToMatch)) {
+ lastError = ERROR_NOT_FOUND;
+ LOG_WARN(("Certificate did not match issuer or name. (%d)", lastError));
+ goto cleanup;
+ }
+
+cleanup:
+ if (signerInfo) {
+ LocalFree(signerInfo);
+ }
+ if (certContext) {
+ CertFreeCertificateContext(certContext);
+ }
+ if (certStore) {
+ CertCloseStore(certStore, 0);
+ }
+ if (cryptMsg) {
+ CryptMsgClose(cryptMsg);
+ }
+ return lastError;
+}
+
+/**
+ * Checks to see if a file stored at filePath matches the specified info.
+ *
+ * @param certContext The certificate context of the file
+ * @param infoToMatch The acceptable information to match
+ * @return FALSE if the info does not match or if any error occurs in the check
+ */
+BOOL
+DoCertificateAttributesMatch(PCCERT_CONTEXT certContext,
+ CertificateCheckInfo &infoToMatch)
+{
+ DWORD dwData;
+ LPWSTR szName = nullptr;
+
+ if (infoToMatch.issuer) {
+ // Pass in nullptr to get the needed size of the issuer buffer.
+ dwData = CertGetNameString(certContext,
+ CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr,
+ nullptr, 0);
+
+ if (!dwData) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for Issuer name buffer.
+ szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName) {
+ LOG_WARN(("Unable to allocate memory for issuer name. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Get Issuer name.
+ if (!CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ CERT_NAME_ISSUER_FLAG, nullptr, szName, dwData)) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.issuer ||
+ wcscmp(szName, infoToMatch.issuer)) {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ LocalFree(szName);
+ szName = nullptr;
+ }
+
+ if (infoToMatch.name) {
+ // Pass in nullptr to get the needed size of the name buffer.
+ dwData = CertGetNameString(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE,
+ 0, nullptr, nullptr, 0);
+ if (!dwData) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ return FALSE;
+ }
+
+ // Allocate memory for the name buffer.
+ szName = (LPWSTR)LocalAlloc(LPTR, dwData * sizeof(WCHAR));
+ if (!szName) {
+ LOG_WARN(("Unable to allocate memory for subject name. (%d)",
+ GetLastError()));
+ return FALSE;
+ }
+
+ // Obtain the name.
+ if (!(CertGetNameStringW(certContext, CERT_NAME_SIMPLE_DISPLAY_TYPE, 0,
+ nullptr, szName, dwData))) {
+ LOG_WARN(("CertGetNameString failed. (%d)", GetLastError()));
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // If the issuer does not match, return a failure.
+ if (!infoToMatch.name ||
+ wcscmp(szName, infoToMatch.name)) {
+ LocalFree(szName);
+ return FALSE;
+ }
+
+ // We have a match!
+ LocalFree(szName);
+ }
+
+ // If there were any errors we would have aborted by now.
+ return TRUE;
+}
+
+/**
+ * Verifies the trust of the specified file path.
+ *
+ * @param filePath The file path to check.
+ * @return ERROR_SUCCESS if successful, or the last error code otherwise.
+ */
+DWORD
+VerifyCertificateTrustForFile(LPCWSTR filePath)
+{
+ // Setup the file to check.
+ WINTRUST_FILE_INFO fileToCheck;
+ ZeroMemory(&fileToCheck, sizeof(fileToCheck));
+ fileToCheck.cbStruct = sizeof(WINTRUST_FILE_INFO);
+ fileToCheck.pcwszFilePath = filePath;
+
+ // Setup what to check, we want to check it is signed and trusted.
+ WINTRUST_DATA trustData;
+ ZeroMemory(&trustData, sizeof(trustData));
+ trustData.cbStruct = sizeof(trustData);
+ trustData.pPolicyCallbackData = nullptr;
+ trustData.pSIPClientData = nullptr;
+ trustData.dwUIChoice = WTD_UI_NONE;
+ trustData.fdwRevocationChecks = WTD_REVOKE_NONE;
+ trustData.dwUnionChoice = WTD_CHOICE_FILE;
+ trustData.dwStateAction = 0;
+ trustData.hWVTStateData = nullptr;
+ trustData.pwszURLReference = nullptr;
+ // no UI
+ trustData.dwUIContext = 0;
+ trustData.pFile = &fileToCheck;
+
+ GUID policyGUID = WINTRUST_ACTION_GENERIC_VERIFY_V2;
+ // Check if the file is signed by something that is trusted.
+ LONG ret = WinVerifyTrust(nullptr, &policyGUID, &trustData);
+ if (ERROR_SUCCESS == ret) {
+ // The hash that represents the subject is trusted and there were no
+ // verification errors. No publisher nor time stamp chain errors.
+ LOG(("The file \"%ls\" is signed and the signature was verified.",
+ filePath));
+ return ERROR_SUCCESS;
+ }
+
+ DWORD lastError = GetLastError();
+ LOG_WARN(("There was an error validating trust of the certificate for file"
+ " \"%ls\". Returned: %d. (%d)", filePath, ret, lastError));
+ return ret;
+}
diff --git a/toolkit/mozapps/update/common/certificatecheck.h b/toolkit/mozapps/update/common/certificatecheck.h
new file mode 100644
index 000000000..43a7c85b6
--- /dev/null
+++ b/toolkit/mozapps/update/common/certificatecheck.h
@@ -0,0 +1,22 @@
+/* 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 _CERTIFICATECHECK_H_
+#define _CERTIFICATECHECK_H_
+
+#include <wincrypt.h>
+
+struct CertificateCheckInfo
+{
+ LPCWSTR name;
+ LPCWSTR issuer;
+};
+
+BOOL DoCertificateAttributesMatch(PCCERT_CONTEXT pCertContext,
+ CertificateCheckInfo &infoToMatch);
+DWORD VerifyCertificateTrustForFile(LPCWSTR filePath);
+DWORD CheckCertificateForPEFile(LPCWSTR filePath,
+ CertificateCheckInfo &infoToMatch);
+
+#endif
diff --git a/toolkit/mozapps/update/common/errors.h b/toolkit/mozapps/update/common/errors.h
new file mode 100644
index 000000000..aac029175
--- /dev/null
+++ b/toolkit/mozapps/update/common/errors.h
@@ -0,0 +1,110 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 Errors_h__
+#define Errors_h__
+
+#define OK 0
+
+// Error codes that are no longer used should not be used again unless they
+// aren't used in client code (e.g. nsUpdateService.js, updates.js,
+// UpdatePrompt.js, etc.).
+
+#define MAR_ERROR_EMPTY_ACTION_LIST 1
+#define LOADSOURCE_ERROR_WRONG_SIZE 2
+
+// Error codes 3-16 are for general update problems.
+#define USAGE_ERROR 3
+#define CRC_ERROR 4
+#define PARSE_ERROR 5
+#define READ_ERROR 6
+#define WRITE_ERROR 7
+// #define UNEXPECTED_ERROR 8 // Replaced with errors 38-42
+#define ELEVATION_CANCELED 9
+#define READ_STRINGS_MEM_ERROR 10
+#define ARCHIVE_READER_MEM_ERROR 11
+#define BSPATCH_MEM_ERROR 12
+#define UPDATER_MEM_ERROR 13
+#define UPDATER_QUOTED_PATH_MEM_ERROR 14
+#define BAD_ACTION_ERROR 15
+#define STRING_CONVERSION_ERROR 16
+
+// Error codes 17-23 are related to security tasks for MAR
+// signing and MAR protection.
+#define CERT_LOAD_ERROR 17
+#define CERT_HANDLING_ERROR 18
+#define CERT_VERIFY_ERROR 19
+#define ARCHIVE_NOT_OPEN 20
+#define COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR 21
+#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
+#define WRITE_ERROR_CALLBACK_APP 37
+#define UNEXPECTED_BZIP_ERROR 39
+#define UNEXPECTED_MAR_ERROR 40
+#define UNEXPECTED_BSPATCH_ERROR 41
+#define UNEXPECTED_FILE_OPERATION_ERROR 42
+#define FILESYSTEM_MOUNT_READWRITE_ERROR 43
+#define DELETE_ERROR_EXPECTED_DIR 46
+#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
+#define WRITE_ERROR_PATCH_FILE 64
+#define WRITE_ERROR_APPLY_DIR_PATH 65
+#define WRITE_ERROR_CALLBACK_PATH 66
+#define WRITE_ERROR_FILE_ACCESS_DENIED 67
+#define WRITE_ERROR_DIR_ACCESS_DENIED 68
+#define WRITE_ERROR_DELETE_BACKUP 69
+#define WRITE_ERROR_EXTRACT 70
+#define REMOVE_FILE_SPEC_ERROR 71
+#define INVALID_APPLYTO_DIR_STAGED_ERROR 72
+#define LOCK_ERROR_PATCH_FILE 73
+#define INVALID_APPLYTO_DIR_ERROR 74
+#define INVALID_INSTALL_DIR_PATH_ERROR 75
+#define INVALID_WORKING_DIR_PATH_ERROR 76
+#define INVALID_CALLBACK_PATH_ERROR 77
+#define INVALID_CALLBACK_DIR_ERROR 78
+
+// Error codes 80 through 99 are reserved for nsUpdateService.js
+
+// The following error codes are only used by updater.exe
+// when a fallback key exists for tests.
+#define FALLBACKKEY_UNKNOWN_ERROR 100
+#define FALLBACKKEY_REGPATH_ERROR 101
+#define FALLBACKKEY_NOKEY_ERROR 102
+#define FALLBACKKEY_SERVICE_NO_STOP_ERROR 103
+#define FALLBACKKEY_LAUNCH_ERROR 104
+
+#endif // Errors_h__
diff --git a/toolkit/mozapps/update/common/moz.build b/toolkit/mozapps/update/common/moz.build
new file mode 100644
index 000000000..cacb0bad2
--- /dev/null
+++ b/toolkit/mozapps/update/common/moz.build
@@ -0,0 +1,32 @@
+# -*- 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/.
+
+EXPORTS += [
+ 'readstrings.h',
+ 'updatecommon.h',
+ 'updatedefines.h',
+]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ EXPORTS += [
+ 'pathhash.h',
+ 'uachelper.h',
+ 'updatehelper.cpp',
+ 'updatehelper.h',
+ ]
+ if CONFIG['MOZ_MAINTENANCE_SERVICE']:
+ EXPORTS += [
+ 'certificatecheck.h',
+ 'registrycertificates.h',
+ ]
+
+Library('updatecommon')
+
+DEFINES['NS_NO_XPCOM'] = True
+
+srcdir = '.'
+
+include('sources.mozbuild')
diff --git a/toolkit/mozapps/update/common/pathhash.cpp b/toolkit/mozapps/update/common/pathhash.cpp
new file mode 100644
index 000000000..89a004cde
--- /dev/null
+++ b/toolkit/mozapps/update/common/pathhash.cpp
@@ -0,0 +1,139 @@
+/* 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 <wincrypt.h>
+#include "pathhash.h"
+
+
+/**
+ * Converts a binary sequence into a hex string
+ *
+ * @param hash The binary data sequence
+ * @param hashSize The size of the binary data sequence
+ * @param hexString A buffer to store the hex string, must be of
+ * size 2 * @hashSize
+*/
+static void
+BinaryDataToHexString(const BYTE *hash, DWORD &hashSize,
+ LPWSTR hexString)
+{
+ WCHAR *p = hexString;
+ for (DWORD i = 0; i < hashSize; ++i) {
+ wsprintfW(p, L"%.2x", hash[i]);
+ p += 2;
+ }
+}
+
+/**
+ * Calculates an MD5 hash for the given input binary data
+ *
+ * @param data Any sequence of bytes
+ * @param dataSize The number of bytes inside @data
+ * @param hash Output buffer to store hash, must be freed by the caller
+ * @param hashSize The number of bytes in the output buffer
+ * @return TRUE on success
+*/
+static BOOL
+CalculateMD5(const char *data, DWORD dataSize,
+ BYTE **hash, DWORD &hashSize)
+{
+ HCRYPTPROV hProv = 0;
+ HCRYPTHASH hHash = 0;
+
+ if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT)) {
+ if (NTE_BAD_KEYSET != GetLastError()) {
+ return FALSE;
+ }
+
+ // Maybe it doesn't exist, try to create it.
+ if (!CryptAcquireContext(&hProv, nullptr, nullptr, PROV_RSA_FULL,
+ CRYPT_VERIFYCONTEXT | CRYPT_NEWKEYSET)) {
+ return FALSE;
+ }
+ }
+
+ if (!CryptCreateHash(hProv, CALG_MD5, 0, 0, &hHash)) {
+ return FALSE;
+ }
+
+ if (!CryptHashData(hHash, reinterpret_cast<const BYTE*>(data),
+ dataSize, 0)) {
+ return FALSE;
+ }
+
+ DWORD dwCount = sizeof(DWORD);
+ if (!CryptGetHashParam(hHash, HP_HASHSIZE, (BYTE *)&hashSize,
+ &dwCount, 0)) {
+ return FALSE;
+ }
+
+ *hash = new BYTE[hashSize];
+ ZeroMemory(*hash, hashSize);
+ if (!CryptGetHashParam(hHash, HP_HASHVAL, *hash, &hashSize, 0)) {
+ return FALSE;
+ }
+
+ if (hHash) {
+ CryptDestroyHash(hHash);
+ }
+
+ if (hProv) {
+ CryptReleaseContext(hProv,0);
+ }
+
+ return TRUE;
+}
+
+/**
+ * Converts a file path into a unique registry location for cert storage
+ *
+ * @param filePath The input file path to get a registry path from
+ * @param registryPath A buffer to write the registry path to, must
+ * be of size in WCHARs MAX_PATH + 1
+ * @return TRUE if successful
+*/
+BOOL
+CalculateRegistryPathFromFilePath(const LPCWSTR filePath,
+ LPWSTR registryPath)
+{
+ size_t filePathLen = wcslen(filePath);
+ if (!filePathLen) {
+ return FALSE;
+ }
+
+ // If the file path ends in a slash, ignore that character
+ if (filePath[filePathLen -1] == L'\\' ||
+ filePath[filePathLen - 1] == L'/') {
+ filePathLen--;
+ }
+
+ // Copy in the full path into our own buffer.
+ // Copying in the extra slash is OK because we calculate the hash
+ // based on the filePathLen which excludes the slash.
+ // +2 to account for the possibly trailing slash and the null terminator.
+ WCHAR *lowercasePath = new WCHAR[filePathLen + 2];
+ memset(lowercasePath, 0, (filePathLen + 2) * sizeof(WCHAR));
+ wcsncpy(lowercasePath, filePath, filePathLen + 1);
+ _wcslwr(lowercasePath);
+
+ BYTE *hash;
+ DWORD hashSize = 0;
+ if (!CalculateMD5(reinterpret_cast<const char*>(lowercasePath),
+ filePathLen * 2,
+ &hash, hashSize)) {
+ delete[] lowercasePath;
+ return FALSE;
+ }
+ delete[] lowercasePath;
+
+ LPCWSTR baseRegPath = L"SOFTWARE\\Mozilla\\"
+ L"MaintenanceService\\";
+ wcsncpy(registryPath, baseRegPath, MAX_PATH);
+ BinaryDataToHexString(hash, hashSize,
+ registryPath + wcslen(baseRegPath));
+ delete[] hash;
+ return TRUE;
+}
diff --git a/toolkit/mozapps/update/common/pathhash.h b/toolkit/mozapps/update/common/pathhash.h
new file mode 100644
index 000000000..a238317e1
--- /dev/null
+++ b/toolkit/mozapps/update/common/pathhash.h
@@ -0,0 +1,19 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _PATHHASH_H_
+#define _PATHHASH_H_
+
+/**
+ * Converts a file path into a unique registry location for cert storage
+ *
+ * @param filePath The input file path to get a registry path from
+ * @param registryPath A buffer to write the registry path to, must
+ * be of size in WCHARs MAX_PATH + 1
+ * @return TRUE if successful
+*/
+BOOL CalculateRegistryPathFromFilePath(const LPCWSTR filePath,
+ LPWSTR registryPath);
+
+#endif
diff --git a/toolkit/mozapps/update/common/readstrings.cpp b/toolkit/mozapps/update/common/readstrings.cpp
new file mode 100644
index 000000000..428c91c0b
--- /dev/null
+++ b/toolkit/mozapps/update/common/readstrings.cpp
@@ -0,0 +1,236 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <limits.h>
+#include <string.h>
+#include <stdio.h>
+#include "readstrings.h"
+#include "errors.h"
+
+#ifdef XP_WIN
+# define NS_tfopen _wfopen
+# define OPEN_MODE L"rb"
+#else
+# define NS_tfopen fopen
+# define OPEN_MODE "r"
+#endif
+
+// stack based FILE wrapper to ensure that fclose is called.
+class AutoFILE {
+public:
+ explicit AutoFILE(FILE *fp) : fp_(fp) {}
+ ~AutoFILE() { if (fp_) fclose(fp_); }
+ operator FILE *() { return fp_; }
+private:
+ FILE *fp_;
+};
+
+class AutoCharArray {
+public:
+ explicit AutoCharArray(size_t len) { ptr_ = new char[len]; }
+ ~AutoCharArray() { delete[] ptr_; }
+ operator char *() { return ptr_; }
+private:
+ char *ptr_;
+};
+
+static const char kNL[] = "\r\n";
+static const char kEquals[] = "=";
+static const char kWhitespace[] = " \t";
+static const char kRBracket[] = "]";
+
+static const char*
+NS_strspnp(const char *delims, const char *str)
+{
+ const char *d;
+ do {
+ for (d = delims; *d != '\0'; ++d) {
+ if (*str == *d) {
+ ++str;
+ break;
+ }
+ }
+ } while (*d);
+
+ return str;
+}
+
+static char*
+NS_strtok(const char *delims, char **str)
+{
+ if (!*str)
+ return nullptr;
+
+ char *ret = (char*) NS_strspnp(delims, *str);
+
+ if (!*ret) {
+ *str = ret;
+ return nullptr;
+ }
+
+ char *i = ret;
+ do {
+ for (const char *d = delims; *d != '\0'; ++d) {
+ if (*i == *d) {
+ *i = '\0';
+ *str = ++i;
+ return ret;
+ }
+ }
+ ++i;
+ } while (*i);
+
+ *str = nullptr;
+ return ret;
+}
+
+/**
+ * Find a key in a keyList containing zero-delimited keys ending with "\0\0".
+ * Returns a zero-based index of the key in the list, or -1 if the key is not found.
+ */
+static int
+find_key(const char *keyList, char* key)
+{
+ if (!keyList)
+ return -1;
+
+ int index = 0;
+ const char *p = keyList;
+ while (*p)
+ {
+ if (strcmp(key, p) == 0)
+ return index;
+
+ p += strlen(p) + 1;
+ index++;
+ }
+
+ // The key was not found if we came here
+ return -1;
+}
+
+/**
+ * A very basic parser for updater.ini taken mostly from nsINIParser.cpp
+ * that can be used by standalone apps.
+ *
+ * @param path Path to the .ini file to read
+ * @param keyList List of zero-delimited keys ending with two zero characters
+ * @param numStrings Number of strings to read into results buffer - must be equal to the number of keys
+ * @param results Two-dimensional array of strings to be filled in the same order as the keys provided
+ * @param section Optional name of the section to read; defaults to "Strings"
+ */
+int
+ReadStrings(const NS_tchar *path,
+ const char *keyList,
+ unsigned int numStrings,
+ char results[][MAX_TEXT_LEN],
+ const char *section)
+{
+ AutoFILE fp(NS_tfopen(path, OPEN_MODE));
+
+ if (!fp)
+ return READ_ERROR;
+
+ /* get file size */
+ if (fseek(fp, 0, SEEK_END) != 0)
+ return READ_ERROR;
+
+ long len = ftell(fp);
+ if (len <= 0)
+ return READ_ERROR;
+
+ size_t flen = size_t(len);
+ AutoCharArray fileContents(flen + 1);
+ if (!fileContents)
+ return READ_STRINGS_MEM_ERROR;
+
+ /* read the file in one swoop */
+ if (fseek(fp, 0, SEEK_SET) != 0)
+ return READ_ERROR;
+
+ size_t rd = fread(fileContents, sizeof(char), flen, fp);
+ if (rd != flen)
+ return READ_ERROR;
+
+ fileContents[flen] = '\0';
+
+ char *buffer = fileContents;
+ bool inStringsSection = false;
+
+ unsigned int read = 0;
+
+ while (char *token = NS_strtok(kNL, &buffer)) {
+ if (token[0] == '#' || token[0] == ';') // it's a comment
+ continue;
+
+ token = (char*) NS_strspnp(kWhitespace, token);
+ if (!*token) // empty line
+ continue;
+
+ if (token[0] == '[') { // section header!
+ ++token;
+ char const * currSection = token;
+
+ char *rb = NS_strtok(kRBracket, &token);
+ if (!rb || NS_strtok(kWhitespace, &token)) {
+ // there's either an unclosed [Section or a [Section]Moretext!
+ // we could frankly decide that this INI file is malformed right
+ // here and stop, but we won't... keep going, looking for
+ // a well-formed [section] to continue working with
+ inStringsSection = false;
+ }
+ else {
+ if (section)
+ inStringsSection = strcmp(currSection, section) == 0;
+ else
+ inStringsSection = strcmp(currSection, "Strings") == 0;
+ }
+
+ continue;
+ }
+
+ if (!inStringsSection) {
+ // If we haven't found a section header (or we found a malformed
+ // section header), or this isn't the [Strings] section don't bother
+ // parsing this line.
+ continue;
+ }
+
+ char *key = token;
+ char *e = NS_strtok(kEquals, &token);
+ if (!e)
+ continue;
+
+ int keyIndex = find_key(keyList, key);
+ if (keyIndex >= 0 && (unsigned int)keyIndex < numStrings)
+ {
+ strncpy(results[keyIndex], token, MAX_TEXT_LEN - 1);
+ results[keyIndex][MAX_TEXT_LEN - 1] = '\0';
+ read++;
+ }
+ }
+
+ return (read == numStrings) ? OK : PARSE_ERROR;
+}
+
+// A wrapper function to read strings for the updater.
+// Added for compatibility with the original code.
+int
+ReadStrings(const NS_tchar *path, StringTable *results)
+{
+ const unsigned int kNumStrings = 2;
+ const char *kUpdaterKeys = "Title\0Info\0";
+ char updater_strings[kNumStrings][MAX_TEXT_LEN];
+
+ int result = ReadStrings(path, kUpdaterKeys, kNumStrings, updater_strings);
+
+ strncpy(results->title, updater_strings[0], MAX_TEXT_LEN - 1);
+ results->title[MAX_TEXT_LEN - 1] = '\0';
+ strncpy(results->info, updater_strings[1], MAX_TEXT_LEN - 1);
+ results->info[MAX_TEXT_LEN - 1] = '\0';
+
+ return result;
+}
diff --git a/toolkit/mozapps/update/common/readstrings.h b/toolkit/mozapps/update/common/readstrings.h
new file mode 100644
index 000000000..cd1909fc9
--- /dev/null
+++ b/toolkit/mozapps/update/common/readstrings.h
@@ -0,0 +1,43 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 READSTRINGS_H__
+#define READSTRINGS_H__
+
+#define MAX_TEXT_LEN 600
+
+#ifdef XP_WIN
+# include <windows.h>
+ typedef WCHAR NS_tchar;
+#else
+ typedef char NS_tchar;
+#endif
+
+#ifndef NULL
+#define NULL 0
+#endif
+
+struct StringTable
+{
+ char title[MAX_TEXT_LEN];
+ char info[MAX_TEXT_LEN];
+};
+
+/**
+ * This function reads in localized strings from updater.ini
+ */
+int ReadStrings(const NS_tchar *path, StringTable *results);
+
+/**
+ * This function reads in localized strings corresponding to the keys from a given .ini
+ */
+int ReadStrings(const NS_tchar *path,
+ const char *keyList,
+ unsigned int numStrings,
+ char results[][MAX_TEXT_LEN],
+ const char *section = nullptr);
+
+#endif // READSTRINGS_H__
diff --git a/toolkit/mozapps/update/common/registrycertificates.cpp b/toolkit/mozapps/update/common/registrycertificates.cpp
new file mode 100644
index 000000000..1c9c44619
--- /dev/null
+++ b/toolkit/mozapps/update/common/registrycertificates.cpp
@@ -0,0 +1,154 @@
+/* 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 <stdio.h>
+#include <stdlib.h>
+#include <windows.h>
+
+#include "registrycertificates.h"
+#include "pathhash.h"
+#include "updatecommon.h"
+#include "updatehelper.h"
+#define MAX_KEY_LENGTH 255
+
+/**
+ * Verifies if the file path matches any certificate stored in the registry.
+ *
+ * @param filePath The file path of the application to check if allowed.
+ * @param allowFallbackKeySkip when this is TRUE the fallback registry key will
+ * be used to skip the certificate check. This is the default since the
+ * fallback registry key is located under HKEY_LOCAL_MACHINE which can't be
+ * written to by a low integrity process.
+ * Note: the maintenance service binary can be used to perform this check for
+ * testing or troubleshooting.
+ * @return TRUE if the binary matches any of the allowed certificates.
+ */
+BOOL
+DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate, LPCWSTR filePath,
+ BOOL allowFallbackKeySkip)
+{
+ WCHAR maintenanceServiceKey[MAX_PATH + 1];
+ if (!CalculateRegistryPathFromFilePath(basePathForUpdate,
+ maintenanceServiceKey)) {
+ return FALSE;
+ }
+
+ // We use KEY_WOW64_64KEY to always force 64-bit view.
+ // The user may have both x86 and x64 applications installed
+ // which each register information. We need a consistent place
+ // to put those certificate attributes in and hence why we always
+ // force the non redirected registry under Wow6432Node.
+ // This flag is ignored on 32bit systems.
+ HKEY baseKey;
+ LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ maintenanceServiceKey, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open key. (%d)", retCode));
+ // 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.
+ retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ TEST_ONLY_FALLBACK_KEY_PATH, 0,
+ KEY_READ | KEY_WOW64_64KEY, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open fallback key. (%d)", retCode));
+ return FALSE;
+ } else if (allowFallbackKeySkip) {
+ LOG_WARN(("Fallback key present, skipping VerifyCertificateTrustForFile "
+ "check and the certificate attribute registry matching "
+ "check."));
+ RegCloseKey(baseKey);
+ return TRUE;
+ }
+ }
+
+ // Get the number of subkeys.
+ DWORD subkeyCount = 0;
+ retCode = RegQueryInfoKeyW(baseKey, nullptr, nullptr, nullptr, &subkeyCount,
+ nullptr, nullptr, nullptr, nullptr, nullptr,
+ nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not query info key. (%d)", retCode));
+ RegCloseKey(baseKey);
+ return FALSE;
+ }
+
+ // Enumerate the subkeys, each subkey represents an allowed certificate.
+ for (DWORD i = 0; i < subkeyCount; i++) {
+ WCHAR subkeyBuffer[MAX_KEY_LENGTH];
+ DWORD subkeyBufferCount = MAX_KEY_LENGTH;
+ retCode = RegEnumKeyExW(baseKey, i, subkeyBuffer,
+ &subkeyBufferCount, nullptr,
+ nullptr, nullptr, nullptr);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not enum certs. (%d)", retCode));
+ RegCloseKey(baseKey);
+ return FALSE;
+ }
+
+ // Open the subkey for the current certificate
+ HKEY subKey;
+ retCode = RegOpenKeyExW(baseKey,
+ subkeyBuffer,
+ 0,
+ KEY_READ | KEY_WOW64_64KEY,
+ &subKey);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not open subkey. (%d)", retCode));
+ continue; // Try the next subkey
+ }
+
+ const int MAX_CHAR_COUNT = 256;
+ DWORD valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ WCHAR name[MAX_CHAR_COUNT] = { L'\0' };
+ WCHAR issuer[MAX_CHAR_COUNT] = { L'\0' };
+
+ // Get the name from the registry
+ retCode = RegQueryValueExW(subKey, L"name", 0, nullptr,
+ (LPBYTE)name, &valueBufSize);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not obtain name from registry. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ // Get the issuer from the registry
+ valueBufSize = MAX_CHAR_COUNT * sizeof(WCHAR);
+ retCode = RegQueryValueExW(subKey, L"issuer", 0, nullptr,
+ (LPBYTE)issuer, &valueBufSize);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Could not obtain issuer from registry. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ CertificateCheckInfo allowedCertificate = {
+ name,
+ issuer,
+ };
+
+ retCode = CheckCertificateForPEFile(filePath, allowedCertificate);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Error on certificate check. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ retCode = VerifyCertificateTrustForFile(filePath);
+ if (retCode != ERROR_SUCCESS) {
+ LOG_WARN(("Error on certificate trust check. (%d)", retCode));
+ RegCloseKey(subKey);
+ continue; // Try the next subkey
+ }
+
+ RegCloseKey(baseKey);
+ // Raise the roof, we found a match!
+ return TRUE;
+ }
+
+ RegCloseKey(baseKey);
+ // No certificates match, :'(
+ return FALSE;
+}
diff --git a/toolkit/mozapps/update/common/registrycertificates.h b/toolkit/mozapps/update/common/registrycertificates.h
new file mode 100644
index 000000000..9f68d1a8d
--- /dev/null
+++ b/toolkit/mozapps/update/common/registrycertificates.h
@@ -0,0 +1,14 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _REGISTRYCERTIFICATES_H_
+#define _REGISTRYCERTIFICATES_H_
+
+#include "certificatecheck.h"
+
+BOOL DoesBinaryMatchAllowedCertificates(LPCWSTR basePathForUpdate,
+ LPCWSTR filePath,
+ BOOL allowFallbackKeySkip = TRUE);
+
+#endif
diff --git a/toolkit/mozapps/update/common/sources.mozbuild b/toolkit/mozapps/update/common/sources.mozbuild
new file mode 100644
index 000000000..cde51e09b
--- /dev/null
+++ b/toolkit/mozapps/update/common/sources.mozbuild
@@ -0,0 +1,28 @@
+# 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/.
+
+sources = []
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'windows':
+ sources += [
+ 'pathhash.cpp',
+ 'uachelper.cpp',
+ 'updatehelper.cpp',
+ ]
+ if CONFIG['MOZ_MAINTENANCE_SERVICE']:
+ sources += [
+ 'certificatecheck.cpp',
+ 'registrycertificates.cpp',
+ ]
+ OS_LIBS += [
+ 'crypt32',
+ 'wintrust',
+ ]
+
+sources += [
+ 'readstrings.cpp',
+ 'updatecommon.cpp',
+]
+
+SOURCES += sorted(['%s/%s' % (srcdir, s) for s in sources])
diff --git a/toolkit/mozapps/update/common/uachelper.cpp b/toolkit/mozapps/update/common/uachelper.cpp
new file mode 100644
index 000000000..11647aa72
--- /dev/null
+++ b/toolkit/mozapps/update/common/uachelper.cpp
@@ -0,0 +1,222 @@
+/* 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 <wtsapi32.h>
+#include "uachelper.h"
+#include "updatecommon.h"
+
+// See the MSDN documentation with title: Privilege Constants
+// At the time of this writing, this documentation is located at:
+// http://msdn.microsoft.com/en-us/library/windows/desktop/bb530716%28v=vs.85%29.aspx
+LPCTSTR UACHelper::PrivsToDisable[] = {
+ SE_ASSIGNPRIMARYTOKEN_NAME,
+ SE_AUDIT_NAME,
+ SE_BACKUP_NAME,
+ // CreateProcess will succeed but the app will fail to launch on some WinXP
+ // machines if SE_CHANGE_NOTIFY_NAME is disabled. In particular this happens
+ // for limited user accounts on those machines. The define is kept here as a
+ // reminder that it should never be re-added.
+ // This permission is for directory watching but also from MSDN: "This
+ // privilege also causes the system to skip all traversal access checks."
+ // SE_CHANGE_NOTIFY_NAME,
+ SE_CREATE_GLOBAL_NAME,
+ SE_CREATE_PAGEFILE_NAME,
+ SE_CREATE_PERMANENT_NAME,
+ SE_CREATE_SYMBOLIC_LINK_NAME,
+ SE_CREATE_TOKEN_NAME,
+ SE_DEBUG_NAME,
+ SE_ENABLE_DELEGATION_NAME,
+ SE_IMPERSONATE_NAME,
+ SE_INC_BASE_PRIORITY_NAME,
+ SE_INCREASE_QUOTA_NAME,
+ SE_INC_WORKING_SET_NAME,
+ SE_LOAD_DRIVER_NAME,
+ SE_LOCK_MEMORY_NAME,
+ SE_MACHINE_ACCOUNT_NAME,
+ SE_MANAGE_VOLUME_NAME,
+ SE_PROF_SINGLE_PROCESS_NAME,
+ SE_RELABEL_NAME,
+ SE_REMOTE_SHUTDOWN_NAME,
+ SE_RESTORE_NAME,
+ SE_SECURITY_NAME,
+ SE_SHUTDOWN_NAME,
+ SE_SYNC_AGENT_NAME,
+ SE_SYSTEM_ENVIRONMENT_NAME,
+ SE_SYSTEM_PROFILE_NAME,
+ SE_SYSTEMTIME_NAME,
+ SE_TAKE_OWNERSHIP_NAME,
+ SE_TCB_NAME,
+ SE_TIME_ZONE_NAME,
+ SE_TRUSTED_CREDMAN_ACCESS_NAME,
+ SE_UNDOCK_NAME,
+ SE_UNSOLICITED_INPUT_NAME
+};
+
+/**
+ * Opens a user token for the given session ID
+ *
+ * @param sessionID The session ID for the token to obtain
+ * @return A handle to the token to obtain which will be primary if enough
+ * permissions exist. Caller should close the handle.
+ */
+HANDLE
+UACHelper::OpenUserToken(DWORD sessionID)
+{
+ HMODULE module = LoadLibraryW(L"wtsapi32.dll");
+ HANDLE token = nullptr;
+ decltype(WTSQueryUserToken)* wtsQueryUserToken =
+ (decltype(WTSQueryUserToken)*) GetProcAddress(module, "WTSQueryUserToken");
+ if (wtsQueryUserToken) {
+ wtsQueryUserToken(sessionID, &token);
+ }
+ FreeLibrary(module);
+ return token;
+}
+
+/**
+ * Opens a linked token for the specified token.
+ *
+ * @param token The token to get the linked token from
+ * @return A linked token or nullptr if one does not exist.
+ * Caller should close the handle.
+ */
+HANDLE
+UACHelper::OpenLinkedToken(HANDLE token)
+{
+ // Magic below...
+ // UAC creates 2 tokens. One is the restricted token which we have.
+ // the other is the UAC elevated one. Since we are running as a service
+ // as the system account we have access to both.
+ TOKEN_LINKED_TOKEN tlt;
+ HANDLE hNewLinkedToken = nullptr;
+ DWORD len;
+ if (GetTokenInformation(token, (TOKEN_INFORMATION_CLASS)TokenLinkedToken,
+ &tlt, sizeof(TOKEN_LINKED_TOKEN), &len)) {
+ token = tlt.LinkedToken;
+ hNewLinkedToken = token;
+ }
+ return hNewLinkedToken;
+}
+
+
+/**
+ * Enables or disables a privilege for the specified token.
+ *
+ * @param token The token to adjust the privilege on.
+ * @param priv The privilege to adjust.
+ * @param enable Whether to enable or disable it
+ * @return TRUE if the token was adjusted to the specified value.
+ */
+BOOL
+UACHelper::SetPrivilege(HANDLE token, LPCTSTR priv, BOOL enable)
+{
+ LUID luidOfPriv;
+ if (!LookupPrivilegeValue(nullptr, priv, &luidOfPriv)) {
+ return FALSE;
+ }
+
+ TOKEN_PRIVILEGES tokenPriv;
+ tokenPriv.PrivilegeCount = 1;
+ tokenPriv.Privileges[0].Luid = luidOfPriv;
+ tokenPriv.Privileges[0].Attributes = enable ? SE_PRIVILEGE_ENABLED : 0;
+
+ SetLastError(ERROR_SUCCESS);
+ if (!AdjustTokenPrivileges(token, false, &tokenPriv,
+ sizeof(tokenPriv), nullptr, nullptr)) {
+ return FALSE;
+ }
+
+ return GetLastError() == ERROR_SUCCESS;
+}
+
+/**
+ * For each privilege that is specified, an attempt will be made to
+ * drop the privilege.
+ *
+ * @param token The token to adjust the privilege on.
+ * Pass nullptr for current token.
+ * @param unneededPrivs An array of unneeded privileges.
+ * @param count The size of the array
+ * @return TRUE if there were no errors
+ */
+BOOL
+UACHelper::DisableUnneededPrivileges(HANDLE token,
+ LPCTSTR *unneededPrivs,
+ size_t count)
+{
+ HANDLE obtainedToken = nullptr;
+ if (!token) {
+ // Note: This handle is a pseudo-handle and need not be closed
+ HANDLE process = GetCurrentProcess();
+ if (!OpenProcessToken(process, TOKEN_ALL_ACCESS_P, &obtainedToken)) {
+ LOG_WARN(("Could not obtain token for current process, no "
+ "privileges changed. (%d)", GetLastError()));
+ return FALSE;
+ }
+ token = obtainedToken;
+ }
+
+ BOOL result = TRUE;
+ for (size_t i = 0; i < count; i++) {
+ if (SetPrivilege(token, unneededPrivs[i], FALSE)) {
+ LOG(("Disabled unneeded token privilege: %s.",
+ unneededPrivs[i]));
+ } else {
+ LOG(("Could not disable token privilege value: %s. (%d)",
+ unneededPrivs[i], GetLastError()));
+ result = FALSE;
+ }
+ }
+
+ if (obtainedToken) {
+ CloseHandle(obtainedToken);
+ }
+ return result;
+}
+
+/**
+ * Disables privileges for the specified token.
+ * The privileges to disable are in PrivsToDisable.
+ * In the future there could be new privs and we are not sure if we should
+ * explicitly disable these or not.
+ *
+ * @param token The token to drop the privilege on.
+ * Pass nullptr for current token.
+ * @return TRUE if there were no errors
+ */
+BOOL
+UACHelper::DisablePrivileges(HANDLE token)
+{
+ static const size_t PrivsToDisableSize =
+ sizeof(UACHelper::PrivsToDisable) / sizeof(UACHelper::PrivsToDisable[0]);
+
+ return DisableUnneededPrivileges(token, UACHelper::PrivsToDisable,
+ PrivsToDisableSize);
+}
+
+/**
+ * Check if the current user can elevate.
+ *
+ * @return true if the user can elevate.
+ * false otherwise.
+ */
+bool
+UACHelper::CanUserElevate()
+{
+ HANDLE token;
+ if (!OpenProcessToken(GetCurrentProcess(), TOKEN_QUERY, &token)) {
+ return false;
+ }
+
+ TOKEN_ELEVATION_TYPE elevationType;
+ DWORD len;
+ bool canElevate = GetTokenInformation(token, TokenElevationType,
+ &elevationType,
+ sizeof(elevationType), &len) &&
+ (elevationType == TokenElevationTypeLimited);
+ CloseHandle(token);
+
+ return canElevate;
+}
diff --git a/toolkit/mozapps/update/common/uachelper.h b/toolkit/mozapps/update/common/uachelper.h
new file mode 100644
index 000000000..6481ff5b5
--- /dev/null
+++ b/toolkit/mozapps/update/common/uachelper.h
@@ -0,0 +1,23 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#ifndef _UACHELPER_H_
+#define _UACHELPER_H_
+
+class UACHelper
+{
+public:
+ static HANDLE OpenUserToken(DWORD sessionID);
+ static HANDLE OpenLinkedToken(HANDLE token);
+ static BOOL DisablePrivileges(HANDLE token);
+ static bool CanUserElevate();
+
+private:
+ static BOOL SetPrivilege(HANDLE token, LPCTSTR privs, BOOL enable);
+ static BOOL DisableUnneededPrivileges(HANDLE token,
+ LPCTSTR *unneededPrivs, size_t count);
+ static LPCTSTR PrivsToDisable[];
+};
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatecommon.cpp b/toolkit/mozapps/update/common/updatecommon.cpp
new file mode 100644
index 000000000..aff7d7260
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatecommon.cpp
@@ -0,0 +1,213 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#if defined(XP_WIN)
+#include <windows.h>
+#endif
+
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#include "updatecommon.h"
+
+UpdateLog::UpdateLog() : logFP(nullptr)
+{
+}
+
+void UpdateLog::Init(NS_tchar* sourcePath,
+ const NS_tchar* fileName)
+{
+ if (logFP) {
+ return;
+ }
+
+ int dstFilePathLen = NS_tsnprintf(mDstFilePath,
+ sizeof(mDstFilePath)/sizeof(mDstFilePath[0]),
+ NS_T("%s/%s"), sourcePath, fileName);
+ // If the destination path was over the length limit,
+ // disable logging by skipping opening the file and setting logFP.
+ if ((dstFilePathLen > 0) &&
+ (dstFilePathLen <
+ static_cast<int>(sizeof(mDstFilePath)/sizeof(mDstFilePath[0])))) {
+#ifdef XP_WIN
+ if (GetTempFileNameW(sourcePath, L"log", 0, mTmpFilePath) != 0) {
+ logFP = NS_tfopen(mTmpFilePath, NS_T("w"));
+
+ // Delete this file now so it is possible to tell from the unelevated
+ // updater process if the elevated updater process has written the log.
+ DeleteFileW(mDstFilePath);
+ }
+#elif XP_MACOSX
+ logFP = NS_tfopen(mDstFilePath, NS_T("w"));
+#else
+ // On platforms that have an updates directory in the installation directory
+ // (e.g. platforms other than Windows and Mac) the update log is written to
+ // a temporary file and then to the update log file. This is needed since
+ // the installation directory is moved during a replace request. This can be
+ // removed when the platform's updates directory is located outside of the
+ // installation directory.
+ logFP = tmpfile();
+#endif
+ }
+}
+
+void UpdateLog::Finish()
+{
+ if (!logFP) {
+ return;
+ }
+
+#if !defined(XP_WIN) && !defined(XP_MACOSX)
+ const int blockSize = 1024;
+ char buffer[blockSize];
+ fflush(logFP);
+ rewind(logFP);
+
+ FILE *updateLogFP = NS_tfopen(mDstFilePath, NS_T("wb+"));
+ while (!feof(logFP)) {
+ size_t read = fread(buffer, 1, blockSize, logFP);
+ if (ferror(logFP)) {
+ fclose(logFP);
+ logFP = nullptr;
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+ return;
+ }
+
+ size_t written = 0;
+
+ while (written < read) {
+ size_t chunkWritten = fwrite(buffer, 1, read - written, updateLogFP);
+ if (chunkWritten <= 0) {
+ fclose(logFP);
+ logFP = nullptr;
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+ return;
+ }
+
+ written += chunkWritten;
+ }
+ }
+ fclose(updateLogFP);
+ updateLogFP = nullptr;
+#endif
+
+ fclose(logFP);
+ logFP = nullptr;
+
+#ifdef XP_WIN
+ // When the log file already exists then the elevated updater has already
+ // written the log file and the temp file for the log should be discarded.
+ if (!NS_taccess(mDstFilePath, F_OK)) {
+ DeleteFileW(mTmpFilePath);
+ } else {
+ MoveFileW(mTmpFilePath, mDstFilePath);
+ }
+#endif
+}
+
+void UpdateLog::Flush()
+{
+ if (!logFP) {
+ return;
+ }
+
+ fflush(logFP);
+}
+
+void UpdateLog::Printf(const char *fmt, ... )
+{
+ if (!logFP) {
+ return;
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ vfprintf(logFP, fmt, ap);
+ fprintf(logFP, "\n");
+ va_end(ap);
+}
+
+void UpdateLog::WarnPrintf(const char *fmt, ... )
+{
+ if (!logFP) {
+ return;
+ }
+
+ va_list ap;
+ va_start(ap, fmt);
+ fprintf(logFP, "*** Warning: ");
+ vfprintf(logFP, fmt, ap);
+ fprintf(logFP, "***\n");
+ va_end(ap);
+}
+
+/**
+ * Performs checks of a full path for validity for this application.
+ *
+ * @param origFullPath
+ * The full path to check.
+ * @return true if the path is valid for this application and false otherwise.
+ */
+bool
+IsValidFullPath(NS_tchar* origFullPath)
+{
+ // Subtract 1 from MAXPATHLEN for null termination.
+ if (NS_tstrlen(origFullPath) > MAXPATHLEN - 1) {
+ // The path is longer than acceptable for this application.
+ return false;
+ }
+
+#ifdef XP_WIN
+ NS_tchar testPath[MAXPATHLEN] = {NS_T('\0')};
+ // GetFullPathNameW will replace / with \ which PathCanonicalizeW requires.
+ if (GetFullPathNameW(origFullPath, MAXPATHLEN, testPath, nullptr) == 0) {
+ // Unable to get the full name for the path (e.g. invalid path).
+ return false;
+ }
+
+ NS_tchar canonicalPath[MAXPATHLEN] = {NS_T('\0')};
+ if (!PathCanonicalizeW(canonicalPath, testPath)) {
+ // Path could not be canonicalized (e.g. invalid path).
+ return false;
+ }
+
+ // Check if the path passed in resolves to a differerent path.
+ if (NS_tstricmp(origFullPath, canonicalPath) != 0) {
+ // Case insensitive string comparison between the supplied path and the
+ // canonical path are not equal. This will prevent directory traversal and
+ // the use of / in paths since they are converted to \.
+ return false;
+ }
+
+ NS_tstrncpy(testPath, origFullPath, MAXPATHLEN);
+ if (!PathStripToRootW(testPath)) {
+ // It should always be possible to strip a valid path to its root.
+ return false;
+ }
+
+ if (origFullPath[0] == NS_T('\\')) {
+ // Only allow UNC server share paths.
+ if (!PathIsUNCServerShareW(testPath)) {
+ return false;
+ }
+ }
+#else
+ // Only allow full paths.
+ if (origFullPath[0] != NS_T('/')) {
+ return false;
+ }
+
+ // The path must not traverse directories
+ if (NS_tstrstr(origFullPath, NS_T("..")) != nullptr ||
+ NS_tstrstr(origFullPath, NS_T("./")) != nullptr) {
+ return false;
+ }
+#endif
+ return true;
+}
diff --git a/toolkit/mozapps/update/common/updatecommon.h b/toolkit/mozapps/update/common/updatecommon.h
new file mode 100644
index 000000000..3055851d8
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatecommon.h
@@ -0,0 +1,47 @@
+/* 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 UPDATECOMMON_H
+#define UPDATECOMMON_H
+
+#include "updatedefines.h"
+#include <stdio.h>
+
+class UpdateLog
+{
+public:
+ static UpdateLog & GetPrimaryLog()
+ {
+ static UpdateLog primaryLog;
+ return primaryLog;
+ }
+
+ void Init(NS_tchar* sourcePath, const NS_tchar* fileName);
+ void Finish();
+ void Flush();
+ void Printf(const char *fmt, ... );
+ void WarnPrintf(const char *fmt, ... );
+
+ ~UpdateLog()
+ {
+ Finish();
+ }
+
+protected:
+ UpdateLog();
+ FILE *logFP;
+ NS_tchar mTmpFilePath[MAXPATHLEN];
+ NS_tchar mDstFilePath[MAXPATHLEN];
+};
+
+bool IsValidFullPath(NS_tchar* fullPath);
+
+#define LOG_WARN(args) UpdateLog::GetPrimaryLog().WarnPrintf args
+#define LOG(args) UpdateLog::GetPrimaryLog().Printf args
+#define LogInit(PATHNAME_, FILENAME_) \
+ UpdateLog::GetPrimaryLog().Init(PATHNAME_, FILENAME_)
+#define LogFinish() UpdateLog::GetPrimaryLog().Finish()
+#define LogFlush() UpdateLog::GetPrimaryLog().Flush()
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatedefines.h b/toolkit/mozapps/update/common/updatedefines.h
new file mode 100644
index 000000000..760d2c4c4
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatedefines.h
@@ -0,0 +1,142 @@
+/* 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 UPDATEDEFINES_H
+#define UPDATEDEFINES_H
+
+#include "readstrings.h"
+
+#ifndef MAXPATHLEN
+# ifdef PATH_MAX
+# define MAXPATHLEN PATH_MAX
+# elif defined(MAX_PATH)
+# define MAXPATHLEN MAX_PATH
+# elif defined(_MAX_PATH)
+# define MAXPATHLEN _MAX_PATH
+# elif defined(CCHMAXPATH)
+# define MAXPATHLEN CCHMAXPATH
+# else
+# define MAXPATHLEN 1024
+# endif
+#endif
+
+#if defined(XP_WIN)
+# include <windows.h>
+# include <shlwapi.h>
+# include <direct.h>
+# include <io.h>
+# include <stdio.h>
+# include <stdarg.h>
+
+# ifndef F_OK
+# define F_OK 00
+# endif
+# ifndef W_OK
+# define W_OK 02
+# endif
+# ifndef R_OK
+# define R_OK 04
+# endif
+# define S_ISDIR(s) (((s) & _S_IFMT) == _S_IFDIR)
+# define S_ISREG(s) (((s) & _S_IFMT) == _S_IFREG)
+
+# define access _access
+
+# define putenv _putenv
+# if defined(_MSC_VER) && _MSC_VER < 1900
+# define stat _stat
+# endif
+# define DELETE_DIR L"tobedeleted"
+# define CALLBACK_BACKUP_EXT L".moz-callback"
+
+# define LOG_S "%S"
+# define NS_T(str) L ## str
+# define NS_SLASH NS_T('\\')
+
+static inline int mywcsprintf(WCHAR* dest, size_t count, const WCHAR* fmt, ...)
+{
+ size_t _count = count - 1;
+ va_list varargs;
+ va_start(varargs, fmt);
+ int result = _vsnwprintf(dest, count - 1, fmt, varargs);
+ va_end(varargs);
+ dest[_count] = L'\0';
+ return result;
+}
+#define NS_tsnprintf mywcsprintf
+# define NS_taccess _waccess
+# define NS_tchdir _wchdir
+# define NS_tchmod _wchmod
+# define NS_tfopen _wfopen
+# define NS_tmkdir(path, perms) _wmkdir(path)
+# define NS_tremove _wremove
+// _wrename is used to avoid the link tracking service.
+# define NS_trename _wrename
+# define NS_trmdir _wrmdir
+# define NS_tstat _wstat
+# define NS_tlstat _wstat // No symlinks on Windows
+# define NS_tstat_t _stat
+# define NS_tstrcat wcscat
+# define NS_tstrcmp wcscmp
+# define NS_tstricmp wcsicmp
+# define NS_tstrcpy wcscpy
+# define NS_tstrncpy wcsncpy
+# define NS_tstrlen wcslen
+# define NS_tstrchr wcschr
+# define NS_tstrrchr wcsrchr
+# define NS_tstrstr wcsstr
+# include "win_dirent.h"
+# define NS_tDIR DIR
+# define NS_tdirent dirent
+# define NS_topendir opendir
+# define NS_tclosedir closedir
+# define NS_treaddir readdir
+#else
+# include <sys/wait.h>
+# include <unistd.h>
+
+#ifdef SOLARIS
+# include <sys/stat.h>
+#else
+# include <fts.h>
+#endif
+# include <dirent.h>
+
+#ifdef XP_MACOSX
+# include <sys/time.h>
+#endif
+
+# define LOG_S "%s"
+# define NS_T(str) str
+# define NS_SLASH NS_T('/')
+# define NS_tsnprintf snprintf
+# define NS_taccess access
+# define NS_tchdir chdir
+# define NS_tchmod chmod
+# define NS_tfopen fopen
+# define NS_tmkdir mkdir
+# define NS_tremove remove
+# define NS_trename rename
+# define NS_trmdir rmdir
+# define NS_tstat stat
+# define NS_tstat_t stat
+# define NS_tlstat lstat
+# define NS_tstrcat strcat
+# define NS_tstrcmp strcmp
+# define NS_tstricmp strcasecmp
+# define NS_tstrcpy strcpy
+# define NS_tstrncpy strncpy
+# define NS_tstrlen strlen
+# define NS_tstrrchr strrchr
+# define NS_tstrstr strstr
+# define NS_tDIR DIR
+# define NS_tdirent dirent
+# define NS_topendir opendir
+# define NS_tclosedir closedir
+# define NS_treaddir readdir
+#endif
+
+#define BACKUP_EXT NS_T(".moz-backup")
+
+#endif
diff --git a/toolkit/mozapps/update/common/updatehelper.cpp b/toolkit/mozapps/update/common/updatehelper.cpp
new file mode 100644
index 000000000..afb89bacf
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatehelper.cpp
@@ -0,0 +1,609 @@
+/* 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>
+
+// Needed for CreateToolhelp32Snapshot
+#include <tlhelp32.h>
+#ifndef ONLY_SERVICE_LAUNCHING
+
+#include <stdio.h>
+#include "shlobj.h"
+#include "updatehelper.h"
+#include "uachelper.h"
+#include "pathhash.h"
+#include "mozilla/UniquePtr.h"
+
+// Needed for PathAppendW
+#include <shlwapi.h>
+
+using mozilla::MakeUnique;
+using mozilla::UniquePtr;
+
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,
+ LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+
+/**
+ * Obtains the path of a file in the same directory as the specified file.
+ *
+ * @param destinationBuffer A buffer of size MAX_PATH + 1 to store the result.
+ * @param siblingFIlePath The path of another file in the same directory
+ * @param newFileName The filename of another file in the same directory
+ * @return TRUE if successful
+ */
+BOOL
+PathGetSiblingFilePath(LPWSTR destinationBuffer,
+ LPCWSTR siblingFilePath,
+ LPCWSTR newFileName)
+{
+ if (wcslen(siblingFilePath) >= MAX_PATH) {
+ return FALSE;
+ }
+
+ wcsncpy(destinationBuffer, siblingFilePath, MAX_PATH);
+ if (!PathRemoveFileSpecW(destinationBuffer)) {
+ return FALSE;
+ }
+
+ if (wcslen(destinationBuffer) + wcslen(newFileName) >= MAX_PATH) {
+ return FALSE;
+ }
+
+ return PathAppendSafe(destinationBuffer, newFileName);
+}
+
+/**
+ * Starts the upgrade process for update of the service if it is
+ * already installed.
+ *
+ * @param installDir the installation directory where
+ * maintenanceservice_installer.exe is located.
+ * @return TRUE if successful
+ */
+BOOL
+StartServiceUpdate(LPCWSTR installDir)
+{
+ // Get a handle to the local computer SCM database
+ SC_HANDLE manager = OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_ALL_ACCESS);
+ if (!manager) {
+ return FALSE;
+ }
+
+ // Open the service
+ SC_HANDLE svc = OpenServiceW(manager, SVC_NAME,
+ SERVICE_ALL_ACCESS);
+ if (!svc) {
+ CloseServiceHandle(manager);
+ return FALSE;
+ }
+
+ // If we reach here, then the service is installed, so
+ // proceed with upgrading it.
+
+ CloseServiceHandle(manager);
+
+ // The service exists and we opened it, get the config bytes needed
+ DWORD bytesNeeded;
+ if (!QueryServiceConfigW(svc, nullptr, 0, &bytesNeeded) &&
+ GetLastError() != ERROR_INSUFFICIENT_BUFFER) {
+ CloseServiceHandle(svc);
+ return FALSE;
+ }
+
+ // Get the service config information, in particular we want the binary
+ // path of the service.
+ UniquePtr<char[]> serviceConfigBuffer = MakeUnique<char[]>(bytesNeeded);
+ if (!QueryServiceConfigW(svc,
+ reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get()),
+ bytesNeeded, &bytesNeeded)) {
+ CloseServiceHandle(svc);
+ return FALSE;
+ }
+
+ CloseServiceHandle(svc);
+
+ QUERY_SERVICE_CONFIGW &serviceConfig =
+ *reinterpret_cast<QUERY_SERVICE_CONFIGW*>(serviceConfigBuffer.get());
+
+ PathUnquoteSpacesW(serviceConfig.lpBinaryPathName);
+
+ // Obtain the temp path of the maintenance service binary
+ WCHAR tmpService[MAX_PATH + 1] = { L'\0' };
+ if (!PathGetSiblingFilePath(tmpService, serviceConfig.lpBinaryPathName,
+ L"maintenanceservice_tmp.exe")) {
+ return FALSE;
+ }
+
+ // Get the new maintenance service path from the install dir
+ WCHAR newMaintServicePath[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(newMaintServicePath, installDir, MAX_PATH);
+ PathAppendSafe(newMaintServicePath,
+ L"maintenanceservice.exe");
+
+ // Copy the temp file in alongside the maintenace service.
+ // This is a requirement for maintenance service upgrades.
+ if (!CopyFileW(newMaintServicePath, tmpService, FALSE)) {
+ return FALSE;
+ }
+
+ // Start the upgrade comparison process
+ STARTUPINFOW si = {0};
+ si.cb = sizeof(STARTUPINFOW);
+ // No particular desktop because no UI
+ si.lpDesktop = L"";
+ PROCESS_INFORMATION pi = {0};
+ WCHAR cmdLine[64] = { '\0' };
+ wcsncpy(cmdLine, L"dummyparam.exe upgrade",
+ sizeof(cmdLine) / sizeof(cmdLine[0]) - 1);
+ BOOL svcUpdateProcessStarted = CreateProcessW(tmpService,
+ cmdLine,
+ nullptr, nullptr, FALSE,
+ 0,
+ nullptr, installDir, &si, &pi);
+ if (svcUpdateProcessStarted) {
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+ return svcUpdateProcessStarted;
+}
+
+#endif
+
+/**
+ * Executes a maintenance service command
+ *
+ * @param argc The total number of arguments in argv
+ * @param argv An array of null terminated strings to pass to the service,
+ * @return ERROR_SUCCESS if the service command was started.
+ * Less than 16000, a windows system error code from StartServiceW
+ * More than 20000, 20000 + the last state of the service constant if
+ * the last state is something other than stopped.
+ * 17001 if the SCM could not be opened
+ * 17002 if the service could not be opened
+*/
+DWORD
+StartServiceCommand(int argc, LPCWSTR* argv)
+{
+ DWORD lastState = WaitForServiceStop(SVC_NAME, 5);
+ if (lastState != SERVICE_STOPPED) {
+ return 20000 + lastState;
+ }
+
+ // Get a handle to the SCM database.
+ SC_HANDLE serviceManager = OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_CONNECT |
+ SC_MANAGER_ENUMERATE_SERVICE);
+ if (!serviceManager) {
+ return 17001;
+ }
+
+ // Get a handle to the service.
+ SC_HANDLE service = OpenServiceW(serviceManager,
+ SVC_NAME,
+ SERVICE_START);
+ if (!service) {
+ CloseServiceHandle(serviceManager);
+ return 17002;
+ }
+
+ // Wait at most 5 seconds trying to start the service in case of errors
+ // like ERROR_SERVICE_DATABASE_LOCKED or ERROR_SERVICE_REQUEST_TIMEOUT.
+ const DWORD maxWaitMS = 5000;
+ DWORD currentWaitMS = 0;
+ DWORD lastError = ERROR_SUCCESS;
+ while (currentWaitMS < maxWaitMS) {
+ BOOL result = StartServiceW(service, argc, argv);
+ if (result) {
+ lastError = ERROR_SUCCESS;
+ break;
+ } else {
+ lastError = GetLastError();
+ }
+ Sleep(100);
+ currentWaitMS += 100;
+ }
+ CloseServiceHandle(service);
+ CloseServiceHandle(serviceManager);
+ return lastError;
+}
+
+#ifndef ONLY_SERVICE_LAUNCHING
+
+/**
+ * Launch a service initiated action for a software update with the
+ * specified arguments.
+ *
+ * @param exePath The path of the executable to run
+ * @param argc The total number of arguments in argv
+ * @param argv An array of null terminated strings to pass to the exePath,
+ * argv[0] must be the path to the updater.exe
+ * @return ERROR_SUCCESS if successful
+ */
+DWORD
+LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR* argv)
+{
+ // The service command is the same as the updater.exe command line except
+ // it has 2 extra args: 1) The Path to udpater.exe, and 2) the command
+ // being executed which is "software-update"
+ LPCWSTR *updaterServiceArgv = new LPCWSTR[argc + 2];
+ updaterServiceArgv[0] = L"MozillaMaintenance";
+ updaterServiceArgv[1] = L"software-update";
+
+ for (int i = 0; i < argc; ++i) {
+ updaterServiceArgv[i + 2] = argv[i];
+ }
+
+ // Execute the service command by starting the service with
+ // the passed in arguments.
+ DWORD ret = StartServiceCommand(argc + 2, updaterServiceArgv);
+ delete[] updaterServiceArgv;
+ return ret;
+}
+
+/**
+ * Joins a base directory path with a filename.
+ *
+ * @param base The base directory path of size MAX_PATH + 1
+ * @param extra The filename to append
+ * @return TRUE if the file name was successful appended to base
+ */
+BOOL
+PathAppendSafe(LPWSTR base, LPCWSTR extra)
+{
+ if (wcslen(base) + wcslen(extra) >= MAX_PATH) {
+ return FALSE;
+ }
+
+ return PathAppendW(base, extra);
+}
+
+/**
+ * Sets update.status to a specific failure code
+ *
+ * @param updateDirPath The path of the update directory
+ * @return TRUE if successful
+ */
+BOOL
+WriteStatusFailure(LPCWSTR updateDirPath, int errorCode)
+{
+ // The temp file is not removed on failure since there is client code that
+ // will remove it.
+ WCHAR tmpUpdateStatusFilePath[MAX_PATH + 1] = { L'\0' };
+ if (GetTempFileNameW(updateDirPath, L"svc", 0, tmpUpdateStatusFilePath) == 0) {
+ return FALSE;
+ }
+
+ HANDLE tmpStatusFile = CreateFileW(tmpUpdateStatusFilePath, GENERIC_WRITE, 0,
+ nullptr, CREATE_ALWAYS, 0, nullptr);
+ if (tmpStatusFile == INVALID_HANDLE_VALUE) {
+ return FALSE;
+ }
+
+ char failure[32];
+ sprintf(failure, "failed: %d", errorCode);
+ DWORD toWrite = strlen(failure);
+ DWORD wrote;
+ BOOL ok = WriteFile(tmpStatusFile, failure,
+ toWrite, &wrote, nullptr);
+ CloseHandle(tmpStatusFile);
+
+ if (!ok || wrote != toWrite) {
+ return FALSE;
+ }
+
+ WCHAR updateStatusFilePath[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(updateStatusFilePath, updateDirPath, MAX_PATH);
+ if (!PathAppendSafe(updateStatusFilePath, L"update.status")) {
+ return FALSE;
+ }
+
+ if (MoveFileExW(tmpUpdateStatusFilePath, updateStatusFilePath,
+ MOVEFILE_REPLACE_EXISTING) == 0) {
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
+#endif
+
+/**
+ * Waits for a service to enter a stopped state.
+ * This function does not stop the service, it just blocks until the service
+ * is stopped.
+ *
+ * @param serviceName The service to wait for.
+ * @param maxWaitSeconds The maximum number of seconds to wait
+ * @return state of the service after a timeout or when stopped.
+ * A value of 255 is returned for an error. Typical values are:
+ * SERVICE_STOPPED 0x00000001
+ * SERVICE_START_PENDING 0x00000002
+ * SERVICE_STOP_PENDING 0x00000003
+ * SERVICE_RUNNING 0x00000004
+ * SERVICE_CONTINUE_PENDING 0x00000005
+ * SERVICE_PAUSE_PENDING 0x00000006
+ * SERVICE_PAUSED 0x00000007
+ * last status not set 0x000000CF
+ * Could no query status 0x000000DF
+ * Could not open service, access denied 0x000000EB
+ * Could not open service, invalid handle 0x000000EC
+ * Could not open service, invalid name 0x000000ED
+ * Could not open service, does not exist 0x000000EE
+ * Could not open service, other error 0x000000EF
+ * Could not open SCM, access denied 0x000000FD
+ * Could not open SCM, database does not exist 0x000000FE;
+ * Could not open SCM, other error 0x000000FF;
+ * Note: The strange choice of error codes above SERVICE_PAUSED are chosen
+ * in case Windows comes out with other service stats higher than 7, they
+ * would likely call it 8 and above. JS code that uses this in TestAUSHelper
+ * only handles values up to 255 so that's why we don't use GetLastError
+ * directly.
+ */
+DWORD
+WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds)
+{
+ // 0x000000CF is defined above to be not set
+ DWORD lastServiceState = 0x000000CF;
+
+ // Get a handle to the SCM database.
+ SC_HANDLE serviceManager = OpenSCManager(nullptr, nullptr,
+ SC_MANAGER_CONNECT |
+ SC_MANAGER_ENUMERATE_SERVICE);
+ if (!serviceManager) {
+ DWORD lastError = GetLastError();
+ switch(lastError) {
+ case ERROR_ACCESS_DENIED:
+ return 0x000000FD;
+ case ERROR_DATABASE_DOES_NOT_EXIST:
+ return 0x000000FE;
+ default:
+ return 0x000000FF;
+ }
+ }
+
+ // Get a handle to the service.
+ SC_HANDLE service = OpenServiceW(serviceManager,
+ serviceName,
+ SERVICE_QUERY_STATUS);
+ if (!service) {
+ DWORD lastError = GetLastError();
+ CloseServiceHandle(serviceManager);
+ switch(lastError) {
+ case ERROR_ACCESS_DENIED:
+ return 0x000000EB;
+ case ERROR_INVALID_HANDLE:
+ return 0x000000EC;
+ case ERROR_INVALID_NAME:
+ return 0x000000ED;
+ case ERROR_SERVICE_DOES_NOT_EXIST:
+ return 0x000000EE;
+ default:
+ return 0x000000EF;
+ }
+ }
+
+ DWORD currentWaitMS = 0;
+ SERVICE_STATUS_PROCESS ssp;
+ ssp.dwCurrentState = lastServiceState;
+ while (currentWaitMS < maxWaitSeconds * 1000) {
+ DWORD bytesNeeded;
+ if (!QueryServiceStatusEx(service, SC_STATUS_PROCESS_INFO, (LPBYTE)&ssp,
+ sizeof(SERVICE_STATUS_PROCESS), &bytesNeeded)) {
+ DWORD lastError = GetLastError();
+ switch (lastError) {
+ case ERROR_INVALID_HANDLE:
+ ssp.dwCurrentState = 0x000000D9;
+ break;
+ case ERROR_ACCESS_DENIED:
+ ssp.dwCurrentState = 0x000000DA;
+ break;
+ case ERROR_INSUFFICIENT_BUFFER:
+ ssp.dwCurrentState = 0x000000DB;
+ break;
+ case ERROR_INVALID_PARAMETER:
+ ssp.dwCurrentState = 0x000000DC;
+ break;
+ case ERROR_INVALID_LEVEL:
+ ssp.dwCurrentState = 0x000000DD;
+ break;
+ case ERROR_SHUTDOWN_IN_PROGRESS:
+ ssp.dwCurrentState = 0x000000DE;
+ break;
+ // These 3 errors can occur when the service is not yet stopped but
+ // it is stopping.
+ case ERROR_INVALID_SERVICE_CONTROL:
+ case ERROR_SERVICE_CANNOT_ACCEPT_CTRL:
+ case ERROR_SERVICE_NOT_ACTIVE:
+ currentWaitMS += 50;
+ Sleep(50);
+ continue;
+ default:
+ ssp.dwCurrentState = 0x000000DF;
+ }
+
+ // We couldn't query the status so just break out
+ break;
+ }
+
+ // The service is already in use.
+ if (ssp.dwCurrentState == SERVICE_STOPPED) {
+ break;
+ }
+ currentWaitMS += 50;
+ Sleep(50);
+ }
+
+ lastServiceState = ssp.dwCurrentState;
+ CloseServiceHandle(service);
+ CloseServiceHandle(serviceManager);
+ return lastServiceState;
+}
+
+#ifndef ONLY_SERVICE_LAUNCHING
+
+/**
+ * Determines if there is at least one process running for the specified
+ * application. A match will be found across any session for any user.
+ *
+ * @param process The process to check for existance
+ * @return ERROR_NOT_FOUND if the process was not found
+ * ERROR_SUCCESS if the process was found and there were no errors
+ * Other Win32 system error code for other errors
+**/
+DWORD
+IsProcessRunning(LPCWSTR filename)
+{
+ // Take a snapshot of all processes in the system.
+ HANDLE snapshot = CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS, 0);
+ if (INVALID_HANDLE_VALUE == snapshot) {
+ return GetLastError();
+ }
+
+ PROCESSENTRY32W processEntry;
+ processEntry.dwSize = sizeof(PROCESSENTRY32W);
+ if (!Process32FirstW(snapshot, &processEntry)) {
+ DWORD lastError = GetLastError();
+ CloseHandle(snapshot);
+ return lastError;
+ }
+
+ do {
+ if (wcsicmp(filename, processEntry.szExeFile) == 0) {
+ CloseHandle(snapshot);
+ return ERROR_SUCCESS;
+ }
+ } while (Process32NextW(snapshot, &processEntry));
+ CloseHandle(snapshot);
+ return ERROR_NOT_FOUND;
+}
+
+/**
+ * Waits for the specified applicaiton to exit.
+ *
+ * @param filename The application to wait for.
+ * @param maxSeconds The maximum amount of seconds to wait for all
+ * instances of the application to exit.
+ * @return ERROR_SUCCESS if no instances of the application exist
+ * WAIT_TIMEOUT if the process is still running after maxSeconds.
+ * Any other Win32 system error code.
+*/
+DWORD
+WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds)
+{
+ DWORD applicationRunningError = WAIT_TIMEOUT;
+ for(DWORD i = 0; i < maxSeconds; i++) {
+ DWORD applicationRunningError = IsProcessRunning(filename);
+ if (ERROR_NOT_FOUND == applicationRunningError) {
+ return ERROR_SUCCESS;
+ }
+ Sleep(1000);
+ }
+
+ if (ERROR_SUCCESS == applicationRunningError) {
+ return WAIT_TIMEOUT;
+ }
+
+ return applicationRunningError;
+}
+
+/**
+ * Determines if the fallback key exists or not
+ *
+ * @return TRUE if the fallback key exists and there was no error checking
+*/
+BOOL
+DoesFallbackKeyExist()
+{
+ HKEY testOnlyFallbackKey;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ TEST_ONLY_FALLBACK_KEY_PATH, 0,
+ KEY_READ | KEY_WOW64_64KEY,
+ &testOnlyFallbackKey) != ERROR_SUCCESS) {
+ return FALSE;
+ }
+
+ RegCloseKey(testOnlyFallbackKey);
+ return TRUE;
+}
+
+#endif
+
+/**
+ * Determines if the file system for the specified file handle is local
+ * @param file path to check the filesystem type for, must be at most MAX_PATH
+ * @param isLocal out parameter which will hold TRUE if the drive is local
+ * @return TRUE if the call succeeded
+*/
+BOOL
+IsLocalFile(LPCWSTR file, BOOL &isLocal)
+{
+ WCHAR rootPath[MAX_PATH + 1] = { L'\0' };
+ if (wcslen(file) > MAX_PATH) {
+ return FALSE;
+ }
+
+ wcsncpy(rootPath, file, MAX_PATH);
+ PathStripToRootW(rootPath);
+ isLocal = GetDriveTypeW(rootPath) == DRIVE_FIXED;
+ return TRUE;
+}
+
+
+/**
+ * Determines the DWORD value of a registry key value
+ *
+ * @param key The base key to where the value name exists
+ * @param valueName The name of the value
+ * @param retValue Out parameter which will hold the value
+ * @return TRUE on success
+*/
+static BOOL
+GetDWORDValue(HKEY key, LPCWSTR valueName, DWORD &retValue)
+{
+ DWORD regDWORDValueSize = sizeof(DWORD);
+ LONG retCode = RegQueryValueExW(key, valueName, 0, nullptr,
+ reinterpret_cast<LPBYTE>(&retValue),
+ &regDWORDValueSize);
+ return ERROR_SUCCESS == retCode;
+}
+
+/**
+ * Determines if the the system's elevation type allows
+ * unprmopted elevation.
+ *
+ * @param isUnpromptedElevation Out parameter which specifies if unprompted
+ * elevation is allowed.
+ * @return TRUE if the user can actually elevate and the value was obtained
+ * successfully.
+*/
+BOOL
+IsUnpromptedElevation(BOOL &isUnpromptedElevation)
+{
+ if (!UACHelper::CanUserElevate()) {
+ return FALSE;
+ }
+
+ LPCWSTR UACBaseRegKey =
+ L"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Policies\\System";
+ HKEY baseKey;
+ LONG retCode = RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ UACBaseRegKey, 0,
+ KEY_READ, &baseKey);
+ if (retCode != ERROR_SUCCESS) {
+ return FALSE;
+ }
+
+ DWORD consent, secureDesktop;
+ BOOL success = GetDWORDValue(baseKey, L"ConsentPromptBehaviorAdmin",
+ consent);
+ success = success &&
+ GetDWORDValue(baseKey, L"PromptOnSecureDesktop", secureDesktop);
+ isUnpromptedElevation = !consent && !secureDesktop;
+
+ RegCloseKey(baseKey);
+ return success;
+}
diff --git a/toolkit/mozapps/update/common/updatehelper.h b/toolkit/mozapps/update/common/updatehelper.h
new file mode 100644
index 000000000..3f010df49
--- /dev/null
+++ b/toolkit/mozapps/update/common/updatehelper.h
@@ -0,0 +1,29 @@
+/* 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 StartServiceUpdate(LPCWSTR installDir);
+DWORD LaunchServiceSoftwareUpdateCommand(int argc, LPCWSTR *argv);
+BOOL WriteStatusFailure(LPCWSTR updateDirPath, int errorCode);
+DWORD WaitForServiceStop(LPCWSTR serviceName, DWORD maxWaitSeconds);
+DWORD WaitForProcessExit(LPCWSTR filename, DWORD maxSeconds);
+DWORD IsProcessRunning(LPCWSTR filename);
+BOOL DoesFallbackKeyExist();
+BOOL IsLocalFile(LPCWSTR file, BOOL &isLocal);
+DWORD StartServiceCommand(int argc, LPCWSTR* argv);
+BOOL IsUnpromptedElevation(BOOL &isUnpromptedElevation);
+
+#define SVC_NAME L"MozillaMaintenance"
+
+#define BASE_SERVICE_REG_KEY \
+ L"SOFTWARE\\Mozilla\\MaintenanceService"
+
+// The test only fallback key, as its name implies, is only present on machines
+// that will use automated tests. Since automated tests always run from a
+// different directory for each test, the presence of this key bypasses the
+// "This is a valid installation directory" check. This key also stores
+// the allowed name and issuer for cert checks so that the cert check
+// code can still be run unchanged.
+#define TEST_ONLY_FALLBACK_KEY_PATH \
+ BASE_SERVICE_REG_KEY L"\\3932ecacee736d366d6436db0f55bce4"
+
diff --git a/toolkit/mozapps/update/common/win_dirent.h b/toolkit/mozapps/update/common/win_dirent.h
new file mode 100644
index 000000000..28f5317ff
--- /dev/null
+++ b/toolkit/mozapps/update/common/win_dirent.h
@@ -0,0 +1,32 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 WINDIRENT_H__
+#define WINDIRENT_H__
+
+#ifndef XP_WIN
+#error This library should only be used on Windows
+#endif
+
+#include <windows.h>
+
+struct DIR {
+ explicit DIR(const WCHAR* path);
+ ~DIR();
+ HANDLE findHandle;
+ WCHAR name[MAX_PATH];
+};
+
+struct dirent {
+ dirent();
+ WCHAR d_name[MAX_PATH];
+};
+
+DIR* opendir(const WCHAR* path);
+int closedir(DIR* dir);
+dirent* readdir(DIR* dir);
+
+#endif // WINDIRENT_H__
diff --git a/toolkit/mozapps/update/content/history.js b/toolkit/mozapps/update/content/history.js
new file mode 100644
index 000000000..32a098de5
--- /dev/null
+++ b/toolkit/mozapps/update/content/history.js
@@ -0,0 +1,70 @@
+/* -*- 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/. */
+
+const NS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+var gUpdateHistory = {
+ _view: null,
+
+ /**
+ * Initialize the User Interface
+ */
+ onLoad: function() {
+ this._view = document.getElementById("historyItems");
+
+ var um =
+ Components.classes["@mozilla.org/updates/update-manager;1"].
+ getService(Components.interfaces.nsIUpdateManager);
+ var uc = um.updateCount;
+ if (uc) {
+ while (this._view.hasChildNodes())
+ this._view.removeChild(this._view.firstChild);
+
+ var bundle = document.getElementById("updateBundle");
+
+ for (var i = 0; i < uc; ++i) {
+ var update = um.getUpdateAt(i);
+ if (!update || !update.name)
+ continue;
+
+ // Don't display updates that are downloading since they don't have
+ // valid statusText for the UI (bug 485493).
+ if (!update.statusText)
+ continue;
+
+ var element = document.createElementNS(NS_XUL, "update");
+ this._view.appendChild(element);
+ element.name = bundle.getFormattedString("updateFullName",
+ [update.name, update.buildID]);
+ element.type = bundle.getString("updateType_" + update.type);
+ element.installDate = this._formatDate(update.installDate);
+ if (update.detailsURL)
+ element.detailsURL = update.detailsURL;
+ else
+ element.hideDetailsURL = true;
+ element.status = update.statusText;
+ }
+ }
+ var cancelbutton = document.documentElement.getButton("cancel");
+ cancelbutton.focus();
+ },
+
+ /**
+ * Formats a date into human readable form
+ * @param seconds
+ * A date in seconds since 1970 epoch
+ * @returns A human readable date string
+ */
+ _formatDate: function(seconds) {
+ var date = new Date(seconds);
+ 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',
+ hour: 'numeric', minute: 'numeric', second: 'numeric' };
+ return date.toLocaleString(locale, dtOptions);
+ }
+};
+
diff --git a/toolkit/mozapps/update/content/history.xul b/toolkit/mozapps/update/content/history.xul
new file mode 100644
index 000000000..92a32b6ef
--- /dev/null
+++ b/toolkit/mozapps/update/content/history.xul
@@ -0,0 +1,39 @@
+<?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 dialog [
+<!ENTITY % historyDTD SYSTEM "chrome://mozapps/locale/update/history.dtd">
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%historyDTD;
+%brandDTD;
+]>
+
+<?xml-stylesheet href="chrome://global/skin/"?>
+<?xml-stylesheet href="chrome://mozapps/content/update/updates.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?>
+
+<dialog id="history" windowtype="Update:History"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ style="width: 35em;"
+ buttons="cancel"
+ defaultButton="cancel"
+ buttonlabelcancel="&closebutton.label;"
+ title="&history.title;"
+ onload="gUpdateHistory.onLoad();">
+
+ <script type="application/javascript"
+ src="chrome://mozapps/content/update/history.js"/>
+
+ <stringbundle id="updateBundle"
+ src="chrome://mozapps/locale/update/updates.properties"/>
+
+ <label>&history.intro;</label>
+ <separator class="thin"/>
+ <richlistbox id="historyItems" flex="1">
+ <label>&noupdates.label;</label>
+ </richlistbox>
+ <separator class="thin"/>
+</dialog>
diff --git a/toolkit/mozapps/update/content/updates.css b/toolkit/mozapps/update/content/updates.css
new file mode 100644
index 000000000..e83c3be03
--- /dev/null
+++ b/toolkit/mozapps/update/content/updates.css
@@ -0,0 +1,33 @@
+/* Stop animations on pages that aren't on the current page (bug 341749). */
+#updates:not([currentpageid="checking"]) #checkingProgress,
+#updates:not([currentpageid="downloading"]) #downloadProgress {
+ display: none;
+}
+
+/* Hide the wizard's header so the size of the billboard can size the window
+ on creation. A custom header will be used in its place when a header is
+ needed. */
+.wizard-header {
+ display: none;
+}
+
+/* Display the custom header */
+.update-header {
+ display: -moz-box !important;
+}
+
+/* Custom header implementation based on the Wizard's header. This allows the
+ size of the billboard's remotecontent to size the window since it does not
+ have an updateheader on the billboard page. */
+updateheader {
+ -moz-binding: url("chrome://mozapps/content/update/updates.xml#updateheader");
+ display: -moz-box;
+ -moz-box-orient: horizontal;
+}
+
+/* Update History Window */
+update {
+ -moz-binding: url("chrome://mozapps/content/update/updates.xml#update");
+ display: -moz-box;
+ -moz-box-orient: vertical;
+}
diff --git a/toolkit/mozapps/update/content/updates.js b/toolkit/mozapps/update/content/updates.js
new file mode 100644
index 000000000..6e8de7275
--- /dev/null
+++ b/toolkit/mozapps/update/content/updates.js
@@ -0,0 +1,1399 @@
+/* -*- 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 */
+
+// Firefox's macBrowserOverlay.xul includes scripts that define Cc, Ci, and Cr
+// so we have to use different names.
+const {classes: CoC, interfaces: CoI, results: CoR, utils: CoU} = Components;
+
+/* globals DownloadUtils, Services, AUSTLMY */
+CoU.import("resource://gre/modules/DownloadUtils.jsm", this);
+CoU.import("resource://gre/modules/Services.jsm", this);
+CoU.import("resource://gre/modules/UpdateTelemetry.jsm", this);
+
+const XMLNS_XUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
+
+const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_CERT_ERRORS = "app.update.cert.errors";
+const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
+const PREF_APP_UPDATE_TEST_LOOP = "app.update.test.loop";
+const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual";
+
+const PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
+
+const UPDATE_TEST_LOOP_INTERVAL = 2000;
+
+const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties";
+
+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_SERVICE = "applied-service";
+const STATE_SUCCEEDED = "succeeded";
+const STATE_DOWNLOAD_FAILED = "download-failed";
+const STATE_FAILED = "failed";
+
+const SRCEVT_FOREGROUND = 1;
+const SRCEVT_BACKGROUND = 2;
+
+const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
+
+var gLogEnabled = false;
+var gUpdatesFoundPageId;
+
+// Notes:
+// 1. use the wizard's goTo method whenever possible to change the wizard
+// page since it is simpler than most other methods and behaves nicely with
+// mochitests.
+// 2. using a page's onPageShow method to then change to a different page will
+// of course call that page's onPageShow method which can make mochitests
+// overly complicated and fragile so avoid doing this if at all possible.
+// This is why a page's next attribute is set prior to the page being shown
+// whenever possible.
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console..
+ */
+function LOG(module, string) {
+ if (gLogEnabled) {
+ dump("*** AUS:UI " + module + ":" + string + "\n");
+ Services.console.logStringMessage("AUS:UI " + module + ":" + string);
+ }
+}
+
+/**
+ * Opens a URL using the event target's url attribute for the URL. This is a
+ * workaround for Bug 263433 which prevents respecting tab browser preferences
+ * for where to open a URL.
+ */
+function openUpdateURL(event) {
+ if (event.button == 0)
+ openURL(event.target.getAttribute("url"));
+}
+
+/**
+ * Gets a preference value, handling the case where there is no default.
+ * @param func
+ * The name of the preference function to call, on nsIPrefBranch
+ * @param preference
+ * The name of the preference
+ * @param defaultValue
+ * The default value to return in the event the preference has
+ * no setting
+ * @returns The value of the preference, or undefined if there was no
+ * user or default value.
+ */
+function getPref(func, preference, defaultValue) {
+ try {
+ return Services.prefs[func](preference);
+ }
+ catch (e) {
+ LOG("General", "getPref - failed to get preference: " + preference);
+ }
+ return defaultValue;
+}
+
+/**
+ * A set of shared data and control functions for the wizard as a whole.
+ */
+var gUpdates = {
+ /**
+ * The nsIUpdate object being used by this window (either for downloading,
+ * notification or both).
+ */
+ update: null,
+
+ /**
+ * The updates.properties <stringbundle> element.
+ */
+ strings: null,
+
+ /**
+ * The Application brandShortName (e.g. "Firefox")
+ */
+ brandName: null,
+
+ /**
+ * The <wizard> element
+ */
+ wiz: null,
+
+ /**
+ * Whether to run the unload handler. This will be set to false when the user
+ * exits the wizard via onWizardCancel or onWizardFinish.
+ */
+ _runUnload: true,
+
+ /**
+ * Submit on close telemtry values for the update wizard.
+ * @param pageID
+ * The page id for the last page displayed.
+ */
+ _submitTelemetry: function(aPageID) {
+ AUSTLMY.pingWizLastPageCode(aPageID);
+ },
+
+ /**
+ * Helper function for setButtons
+ * Resets button to original label & accesskey if string is null.
+ */
+ _setButton: function(button, string) {
+ if (string) {
+ var label = this.getAUSString(string);
+ if (label.indexOf("%S") != -1)
+ label = label.replace(/%S/, this.brandName);
+ button.label = label;
+ button.setAttribute("accesskey",
+ this.getAUSString(string + ".accesskey"));
+ } else {
+ button.label = button.defaultLabel;
+ button.setAttribute("accesskey", button.defaultAccesskey);
+ }
+ },
+
+ /**
+ * Sets the attributes needed for this Wizard's control buttons (labels,
+ * disabled, hidden, etc.)
+ * @param extra1ButtonString
+ * The property in the stringbundle containing the label to put on
+ * the first extra button, or null to hide the first extra button.
+ * @param extra2ButtonString
+ * The property in the stringbundle containing the label to put on
+ * the second extra button, or null to hide the second extra button.
+ * @param nextFinishButtonString
+ * The property in the stringbundle containing the label to put on
+ * the Next / Finish button, or null to hide the button. The Next and
+ * Finish buttons are never displayed at the same time in a wizard
+ * with the the Finish button only being displayed when there are no
+ * additional pages to display in the wizard.
+ * @param canAdvance
+ * true if the wizard can be advanced (e.g. the next / finish button
+ * should be enabled), false otherwise.
+ * @param showCancel
+ * true if the wizard's cancel button should be shown, false
+ * otherwise. If not specified this will default to false.
+ *
+ * Note:
+ * Per Bug 324121 the wizard should not look like a wizard and to accomplish
+ * this the back button is never displayed. This causes the wizard buttons to
+ * be arranged as follows on Windows with the next and finish buttons never
+ * being displayed at the same time.
+ * +--------------------------------------------------------------+
+ * | [ extra1 ] [ extra2 ] [ next or finish ] |
+ * +--------------------------------------------------------------+
+ */
+ setButtons: function(extra1ButtonString, extra2ButtonString,
+ nextFinishButtonString, canAdvance, showCancel) {
+ this.wiz.canAdvance = canAdvance;
+
+ var bnf = this.wiz.getButton(this.wiz.onLastPage ? "finish" : "next");
+ var be1 = this.wiz.getButton("extra1");
+ var be2 = this.wiz.getButton("extra2");
+ var bc = this.wiz.getButton("cancel");
+
+ // Set the labels for the next / finish, extra1, and extra2 buttons
+ this._setButton(bnf, nextFinishButtonString);
+ this._setButton(be1, extra1ButtonString);
+ this._setButton(be2, extra2ButtonString);
+
+ bnf.hidden = bnf.disabled = !nextFinishButtonString;
+ be1.hidden = be1.disabled = !extra1ButtonString;
+ be2.hidden = be2.disabled = !extra2ButtonString;
+ bc.hidden = bc.disabled = !showCancel;
+
+ // Hide and disable the back button each time setButtons is called
+ // (see bug 464765).
+ var btn = this.wiz.getButton("back");
+ btn.hidden = btn.disabled = true;
+
+ // Hide and disable the finish button if not on the last page or the next
+ // button if on the last page each time setButtons is called.
+ btn = this.wiz.getButton(this.wiz.onLastPage ? "next" : "finish");
+ btn.hidden = btn.disabled = true;
+ },
+
+ getAUSString: function(key, strings) {
+ if (strings)
+ return this.strings.getFormattedString(key, strings);
+ return this.strings.getString(key);
+ },
+
+ never: function () {
+ // If the user clicks "No Thanks", we should not prompt them to update to
+ // this version again unless they manually select "Check for Updates..."
+ // which will clear all of the "never" prefs. There are currently two
+ // "never" prefs: the older PREFBRANCH_APP_UPDATE_NEVER as well as the
+ // OSX-only PREF_APP_UPDATE_ELEVATE_NEVER. We set both of these prefs (if
+ // applicable) to ensure that we don't prompt the user regardless of which
+ // pref is checked.
+ let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + this.update.appVersion;
+ Services.prefs.setBoolPref(neverPrefName, true);
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (aus.elevationRequired) {
+ Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_NEVER,
+ this.update.appVersion);
+ }
+ },
+
+ /**
+ * A hash of |pageid| attribute to page object. Can be used to dispatch
+ * function calls to the appropriate page.
+ */
+ _pages: { },
+
+ /**
+ * Called when the user presses the "Finish" button on the wizard, dispatches
+ * the function call to the selected page.
+ */
+ onWizardFinish: function() {
+ this._runUnload = false;
+ var pageid = document.documentElement.currentPage.pageid;
+ if ("onWizardFinish" in this._pages[pageid])
+ this._pages[pageid].onWizardFinish();
+ this._submitTelemetry(pageid);
+ },
+
+ /**
+ * Called when the user presses the "Cancel" button on the wizard, dispatches
+ * the function call to the selected page.
+ */
+ onWizardCancel: function() {
+ this._runUnload = false;
+ var pageid = document.documentElement.currentPage.pageid;
+ if ("onWizardCancel" in this._pages[pageid])
+ this._pages[pageid].onWizardCancel();
+ this._submitTelemetry(pageid);
+ },
+
+ /**
+ * Called when the user presses the "Next" button on the wizard, dispatches
+ * the function call to the selected page.
+ */
+ onWizardNext: function() {
+ var cp = document.documentElement.currentPage;
+ if (!cp)
+ return;
+ var pageid = cp.pageid;
+ if ("onWizardNext" in this._pages[pageid])
+ this._pages[pageid].onWizardNext();
+ },
+
+ /**
+ * The checking process that spawned this update UI. There are two types:
+ * SRCEVT_FOREGROUND:
+ * Some user-generated event caused this UI to appear, e.g. the Help
+ * menu item or the button in preferences. When in this mode, the UI
+ * should remain active for the duration of the download.
+ * SRCEVT_BACKGROUND:
+ * A background update check caused this UI to appear, probably because
+ * the user has the app.update.auto preference set to false.
+ */
+ sourceEvent: SRCEVT_FOREGROUND,
+
+ /**
+ * Helper function for onLoad
+ * Saves default button label & accesskey for use by _setButton
+ */
+ _cacheButtonStrings: function (buttonName) {
+ var button = this.wiz.getButton(buttonName);
+ button.defaultLabel = button.label;
+ button.defaultAccesskey = button.getAttribute("accesskey");
+ },
+
+ /**
+ * Called when the wizard UI is loaded.
+ */
+ onLoad: function() {
+ this.wiz = document.documentElement;
+
+ gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
+
+ this.strings = document.getElementById("updateStrings");
+ var brandStrings = document.getElementById("brandStrings");
+ this.brandName = brandStrings.getString("brandShortName");
+
+ var pages = this.wiz.childNodes;
+ for (var i = 0; i < pages.length; ++i) {
+ var page = pages[i];
+ if (page.localName == "wizardpage")
+ this._pages[page.pageid] = eval(page.getAttribute("object"));
+ }
+
+ // Cache the standard button labels in case we need to restore them
+ this._cacheButtonStrings("next");
+ this._cacheButtonStrings("finish");
+ this._cacheButtonStrings("extra1");
+ this._cacheButtonStrings("extra2");
+
+ // Advance to the Start page.
+ this.getStartPageID(function(startPageID) {
+ LOG("gUpdates", "onLoad - setting current page to startpage " + startPageID);
+ gUpdates.wiz.currentPage = document.getElementById(startPageID);
+ });
+ },
+
+ /**
+ * Called when the wizard UI is unloaded.
+ */
+ onUnload: function() {
+ if (this._runUnload) {
+ var cp = this.wiz.currentPage;
+ if (cp.pageid != "finished" && cp.pageid != "finishedBackground")
+ this.onWizardCancel();
+ }
+ },
+
+ /**
+ * Gets the ID of the <wizardpage> object that should be displayed first. This
+ * is an asynchronous method that passes the resulting object to a callback
+ * function.
+ *
+ * This is determined by how we were called by the update prompt:
+ *
+ * Prompt Method: Arg0: Update State: Src Event: Failed: Result:
+ * showUpdateAvailable nsIUpdate obj -- background -- updatesfoundbasic
+ * showUpdateDownloaded nsIUpdate obj pending background -- finishedBackground
+ * showUpdateError nsIUpdate obj failed either partial errorpatching
+ * showUpdateError nsIUpdate obj failed either complete errors
+ * checkForUpdates null -- foreground -- checking
+ * checkForUpdates null downloading foreground -- downloading
+ *
+ * @param aCallback
+ * A callback to pass the <wizardpage> object to be displayed first to.
+ */
+ getStartPageID: function(aCallback) {
+ if ("arguments" in window && window.arguments[0]) {
+ var arg0 = window.arguments[0];
+ if (arg0 instanceof CoI.nsIUpdate) {
+ // If the first argument is a nsIUpdate object, we are notifying the
+ // user that the background checking found an update that requires
+ // their permission to install, and it's ready for download.
+ this.setUpdate(arg0);
+ if (this.update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) {
+ aCallback("errorextra");
+ return;
+ }
+
+ if (this.update.unsupported) {
+ aCallback("unsupported");
+ return;
+ }
+
+ var p = this.update.selectedPatch;
+ if (p) {
+ let state = p.state;
+ let patchFailed = this.update.getProperty("patchingFailed");
+ if (patchFailed) {
+ if (patchFailed != "partial" || this.update.patchCount != 2) {
+ // If the complete patch failed, which is far less likely, show
+ // the error text held by the update object in the generic errors
+ // page, triggered by the |STATE_DOWNLOAD_FAILED| state. This also
+ // handles the case when an elevation was cancelled on Mac OS X.
+ state = STATE_DOWNLOAD_FAILED;
+ } else {
+ // If the system failed to apply the partial patch, show the
+ // screen which best describes this condition, which is triggered
+ // by the |STATE_FAILED| state.
+ state = STATE_FAILED;
+ }
+ }
+
+ // Now select the best page to start with, given the current state of
+ // the Update.
+ switch (state) {
+ case STATE_PENDING:
+ case STATE_PENDING_SERVICE:
+ case STATE_PENDING_ELEVATE:
+ case STATE_APPLIED:
+ case STATE_APPLIED_SERVICE:
+ this.sourceEvent = SRCEVT_BACKGROUND;
+ aCallback("finishedBackground");
+ return;
+ case STATE_DOWNLOADING:
+ aCallback("downloading");
+ return;
+ case STATE_FAILED:
+ window.getAttention();
+ aCallback("errorpatching");
+ return;
+ case STATE_DOWNLOAD_FAILED:
+ case STATE_APPLYING:
+ aCallback("errors");
+ return;
+ }
+ }
+
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (!aus.canApplyUpdates) {
+ aCallback("manualUpdate");
+ return;
+ }
+
+ aCallback(this.updatesFoundPageId);
+ return;
+ }
+ }
+ else {
+ var um = CoC["@mozilla.org/updates/update-manager;1"].
+ getService(CoI.nsIUpdateManager);
+ if (um.activeUpdate) {
+ this.setUpdate(um.activeUpdate);
+ aCallback("downloading");
+ return;
+ }
+ }
+ aCallback("checking");
+ },
+
+ /**
+ * Returns the string page ID for the appropriate updates found page based
+ * on the update's metadata.
+ */
+ get updatesFoundPageId() {
+ if (gUpdatesFoundPageId)
+ return gUpdatesFoundPageId;
+ return gUpdatesFoundPageId = "updatesfoundbasic";
+ },
+
+ /**
+ * Sets the Update object for this wizard
+ * @param update
+ * The update object
+ */
+ setUpdate: function(update) {
+ this.update = update;
+ if (this.update)
+ this.update.QueryInterface(CoI.nsIWritablePropertyBag);
+ }
+};
+
+/**
+ * The "Checking for Updates" page. Provides feedback on the update checking
+ * process.
+ */
+var gCheckingPage = {
+ /**
+ * The nsIUpdateChecker that is currently checking for updates. We hold onto
+ * this so we can cancel the update check if the user closes the window.
+ */
+ _checker: null,
+
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ gUpdates.setButtons(null, null, null, false, true);
+ gUpdates.wiz.getButton("cancel").focus();
+
+ // Clear all of the "never" prefs to handle the scenario where the user
+ // clicked "never" for an update, selected "Check for Updates...", and
+ // then canceled. If we don't clear the "never" prefs future
+ // notifications will never happen.
+ Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER);
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
+ }
+
+ // The user will be notified if there is an error so clear the background
+ // check error count.
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
+ }
+
+ // The preference will be set back to true if the system is still
+ // unsupported.
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED);
+ }
+
+ this._checker = CoC["@mozilla.org/updates/update-checker;1"].
+ createInstance(CoI.nsIUpdateChecker);
+ this._checker.checkForUpdates(this.updateListener, true);
+ },
+
+ /**
+ * The user has closed the window, either by pressing cancel or using a Window
+ * Manager control, so stop checking for updates.
+ */
+ onWizardCancel: function() {
+ this._checker.stopChecking(CoI.nsIUpdateChecker.CURRENT_CHECK);
+ },
+
+ /**
+ * An object implementing nsIUpdateCheckListener that is notified as the
+ * update check commences.
+ */
+ updateListener: {
+ /**
+ * See nsIUpdateCheckListener
+ */
+ onCheckComplete: function(request, updates, updateCount) {
+ var aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ gUpdates.setUpdate(aus.selectUpdate(updates, updates.length));
+ if (gUpdates.update) {
+ LOG("gCheckingPage", "onCheckComplete - update found");
+ if (gUpdates.update.unsupported) {
+ gUpdates.wiz.goTo("unsupported");
+ return;
+ }
+
+ if (!aus.canApplyUpdates || gUpdates.update.elevationFailure) {
+ // Prevent multiple notifications for the same update when the user is
+ // unable to apply updates.
+ gUpdates.never();
+ gUpdates.wiz.goTo("manualUpdate");
+ return;
+ }
+
+ gUpdates.wiz.goTo(gUpdates.updatesFoundPageId);
+ return;
+ }
+
+ LOG("gCheckingPage", "onCheckComplete - no update found");
+ gUpdates.wiz.goTo("noupdatesfound");
+ },
+
+ /**
+ * See nsIUpdateCheckListener
+ */
+ onError: function(request, update) {
+ LOG("gCheckingPage", "onError - proceeding to error page");
+ gUpdates.setUpdate(update);
+ gUpdates.wiz.goTo("errors");
+ },
+
+ /**
+ * See nsISupports.idl
+ */
+ QueryInterface: function(aIID) {
+ if (!aIID.equals(CoI.nsIUpdateCheckListener) &&
+ !aIID.equals(CoI.nsISupports))
+ throw CoR.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+ }
+};
+
+/**
+ * The "No Updates Are Available" page
+ */
+var gNoUpdatesPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ LOG("gNoUpdatesPage", "onPageShow - could not select an appropriate " +
+ "update. Either there were no updates or |selectUpdate| failed");
+
+ if (getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true))
+ document.getElementById("noUpdatesAutoEnabled").hidden = false;
+ else
+ document.getElementById("noUpdatesAutoDisabled").hidden = false;
+
+ gUpdates.setButtons(null, null, "okButton", true);
+ gUpdates.wiz.getButton("finish").focus();
+ }
+};
+
+/**
+ * The "Unable to Update" page. Provides the user information about why they
+ * were unable to update and a manual download url.
+ */
+var gManualUpdatePage = {
+ onPageShow: function() {
+ var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL);
+ var manualUpdateLinkLabel = document.getElementById("manualUpdateLinkLabel");
+ manualUpdateLinkLabel.value = manualURL;
+ manualUpdateLinkLabel.setAttribute("url", manualURL);
+
+ gUpdates.setButtons(null, null, "okButton", true);
+ gUpdates.wiz.getButton("finish").focus();
+ }
+};
+
+/**
+ * The "System Unsupported" page. Provides the user with information about their
+ * system no longer being supported and an url for more information.
+ */
+var gUnsupportedPage = {
+ onPageShow: function() {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true);
+ if (gUpdates.update.detailsURL) {
+ let unsupportedLinkLabel = document.getElementById("unsupportedLinkLabel");
+ unsupportedLinkLabel.setAttribute("url", gUpdates.update.detailsURL);
+ }
+
+ gUpdates.setButtons(null, null, "okButton", true);
+ gUpdates.wiz.getButton("finish").focus();
+ }
+};
+
+/**
+ * The "Updates Are Available" page. Provides the user information about the
+ * available update.
+ */
+var gUpdatesFoundBasicPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ gUpdates.wiz.canRewind = false;
+ var update = gUpdates.update;
+ gUpdates.setButtons("askLaterButton",
+ update.showNeverForVersion ? "noThanksButton" : null,
+ "updateButton_" + update.type, true);
+ var btn = gUpdates.wiz.getButton("next");
+ btn.focus();
+
+ var updateName = update.name;
+ if (update.channel == "nightly") {
+ updateName = gUpdates.getAUSString("updateNightlyName",
+ [gUpdates.brandName,
+ update.displayVersion,
+ update.buildID]);
+ }
+ var updateNameElement = document.getElementById("updateName");
+ updateNameElement.value = updateName;
+
+ var introText = gUpdates.getAUSString("intro_" + update.type,
+ [gUpdates.brandName, update.displayVersion]);
+ var introElem = document.getElementById("updatesFoundInto");
+ introElem.setAttribute("severity", update.type);
+ introElem.textContent = introText;
+
+ var updateMoreInfoURL = document.getElementById("updateMoreInfoURL");
+ if (update.detailsURL)
+ updateMoreInfoURL.setAttribute("url", update.detailsURL);
+ else
+ updateMoreInfoURL.hidden = true;
+
+ var updateTitle = gUpdates.getAUSString("updatesfound_" + update.type +
+ ".title");
+ document.getElementById("updatesFoundBasicHeader").setAttribute("label", updateTitle);
+ },
+
+ onExtra1: function() {
+ gUpdates.wiz.cancel();
+ },
+
+ onExtra2: function() {
+ gUpdates.never();
+ gUpdates.wiz.cancel();
+ }
+};
+
+/**
+ * The "Update is Downloading" page - provides feedback for the download
+ * process plus a pause/resume UI
+ */
+var gDownloadingPage = {
+ /**
+ * DOM Elements
+ */
+ _downloadStatus: null,
+ _downloadProgress: null,
+ _pauseButton: null,
+
+ /**
+ * Whether or not we are currently paused
+ */
+ _paused: false,
+
+ /**
+ * Label cache to hold the 'Connecting' string
+ */
+ _label_downloadStatus: null,
+
+ /**
+ * Member variables for updating download status
+ */
+ _lastSec: Infinity,
+ _startTime: null,
+ _pausedStatus: "",
+
+ _hiding: false,
+
+ /**
+ * Have we registered an observer for a background update being staged
+ */
+ _updateApplyingObserver: false,
+
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ this._downloadStatus = document.getElementById("downloadStatus");
+ this._downloadProgress = document.getElementById("downloadProgress");
+ this._pauseButton = document.getElementById("pauseButton");
+ this._label_downloadStatus = this._downloadStatus.textContent;
+
+ this._pauseButton.setAttribute("tooltiptext",
+ gUpdates.getAUSString("pauseButtonPause"));
+
+ // move focus to the pause/resume button and then disable it (bug #353177)
+ this._pauseButton.focus();
+ this._pauseButton.disabled = true;
+
+ var aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+
+ var um = CoC["@mozilla.org/updates/update-manager;1"].
+ getService(CoI.nsIUpdateManager);
+ var activeUpdate = um.activeUpdate;
+ if (activeUpdate) {
+ gUpdates.setUpdate(activeUpdate);
+
+ // It's possible the update has already been downloaded and is being
+ // applied by the time this page is shown, depending on how fast the
+ // download goes and how quickly the 'next' button is clicked to get here.
+ if (activeUpdate.state == STATE_PENDING ||
+ activeUpdate.state == STATE_PENDING_ELEVATE ||
+ activeUpdate.state == STATE_PENDING_SERVICE) {
+ if (!activeUpdate.getProperty("stagingFailed")) {
+ gUpdates.setButtons("hideButton", null, null, false);
+ gUpdates.wiz.getButton("extra1").focus();
+
+ this._setUpdateApplying();
+ return;
+ }
+
+ gUpdates.wiz.goTo("finished");
+ return;
+ }
+ }
+
+ if (!gUpdates.update) {
+ LOG("gDownloadingPage", "onPageShow - no valid update to download?!");
+ return;
+ }
+
+ this._startTime = Date.now();
+
+ try {
+ // Say that this was a foreground download, not a background download,
+ // since the user cared enough to look in on this process.
+ gUpdates.update.QueryInterface(CoI.nsIWritablePropertyBag);
+ gUpdates.update.setProperty("foregroundDownload", "true");
+
+ // Pause any active background download and restart it as a foreground
+ // download.
+ aus.pauseDownload();
+ var state = aus.downloadUpdate(gUpdates.update, false);
+ if (state == "failed") {
+ // We've tried as hard as we could to download a valid update -
+ // we fell back from a partial patch to a complete patch and even
+ // then we couldn't validate. Show a validation error with instructions
+ // on how to manually update.
+ this.cleanUp();
+ gUpdates.wiz.goTo("errors");
+ return;
+ }
+ // Add this UI as a listener for active downloads
+ aus.addDownloadListener(this);
+
+ if (activeUpdate)
+ this._setUIState(!aus.isDownloading);
+ }
+ catch (e) {
+ LOG("gDownloadingPage", "onPageShow - error: " + e);
+ }
+
+ gUpdates.setButtons("hideButton", null, null, false);
+ gUpdates.wiz.getButton("extra1").focus();
+ },
+
+ /**
+ * Updates the text status message
+ */
+ _setStatus: function(status) {
+ // Don't bother setting the same text more than once. This can happen
+ // due to the asynchronous behavior of the downloader.
+ if (this._downloadStatus.textContent == status)
+ return;
+ while (this._downloadStatus.hasChildNodes())
+ this._downloadStatus.removeChild(this._downloadStatus.firstChild);
+ this._downloadStatus.appendChild(document.createTextNode(status));
+ },
+
+ /**
+ * Update download progress status to show time left, speed, and progress.
+ * Also updates the status needed for pausing the download.
+ *
+ * @param aCurr
+ * Current number of bytes transferred
+ * @param aMax
+ * Total file size of the download
+ * @return Current active download status
+ */
+ _updateDownloadStatus: function(aCurr, aMax) {
+ let status;
+
+ // Get the download time left and progress
+ let rate = aCurr / (Date.now() - this._startTime) * 1000;
+ [status, this._lastSec] =
+ DownloadUtils.getDownloadStatus(aCurr, aMax, rate, this._lastSec);
+
+ // Get the download progress for pausing
+ this._pausedStatus = DownloadUtils.getTransferTotal(aCurr, aMax);
+
+ return status;
+ },
+
+ /**
+ * Adjust UI to suit a certain state of paused-ness
+ * @param paused
+ * Whether or not the download is paused
+ */
+ _setUIState: function(paused) {
+ var u = gUpdates.update;
+ if (paused) {
+ if (this._downloadProgress.mode != "normal")
+ this._downloadProgress.mode = "normal";
+ this._pauseButton.setAttribute("tooltiptext",
+ gUpdates.getAUSString("pauseButtonResume"));
+ this._pauseButton.setAttribute("paused", "true");
+ var p = u.selectedPatch.QueryInterface(CoI.nsIPropertyBag);
+ var status = p.getProperty("status");
+ if (status) {
+ let pausedStatus = gUpdates.getAUSString("downloadPausedStatus", [status]);
+ this._setStatus(pausedStatus);
+ }
+ }
+ else {
+ if (this._downloadProgress.mode != "undetermined")
+ this._downloadProgress.mode = "undetermined";
+ this._pauseButton.setAttribute("paused", "false");
+ this._pauseButton.setAttribute("tooltiptext",
+ gUpdates.getAUSString("pauseButtonPause"));
+ this._setStatus(this._label_downloadStatus);
+ }
+ },
+
+ /**
+ * Wait for an update being staged in the background.
+ */
+ _setUpdateApplying: function() {
+ this._downloadProgress.mode = "undetermined";
+ this._pauseButton.hidden = true;
+ let applyingStatus = gUpdates.getAUSString("applyingUpdate");
+ this._setStatus(applyingStatus);
+
+ Services.obs.addObserver(this, "update-staged", false);
+ this._updateApplyingObserver = true;
+ },
+
+ /**
+ * Clean up the listener and observer registered for the wizard.
+ */
+ cleanUp: function() {
+ var aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ aus.removeDownloadListener(this);
+
+ if (this._updateApplyingObserver) {
+ Services.obs.removeObserver(this, "update-staged");
+ this._updateApplyingObserver = false;
+ }
+ },
+
+ /**
+ * When the user clicks the Pause/Resume button
+ */
+ onPause: function() {
+ var aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (this._paused)
+ aus.downloadUpdate(gUpdates.update, false);
+ else {
+ var patch = gUpdates.update.selectedPatch;
+ patch.QueryInterface(CoI.nsIWritablePropertyBag);
+ patch.setProperty("status", this._pausedStatus);
+ aus.pauseDownload();
+ }
+ this._paused = !this._paused;
+
+ // Update the UI
+ this._setUIState(this._paused);
+ },
+
+ /**
+ * When the user has closed the window using a Window Manager control (this
+ * page doesn't have a cancel button) cancel the update in progress.
+ */
+ onWizardCancel: function() {
+ if (this._hiding)
+ return;
+
+ this.cleanUp();
+ },
+
+ /**
+ * When the user closes the Wizard UI by clicking the Hide button
+ */
+ onHide: function() {
+ // Set _hiding to true to prevent onWizardCancel from cancelling the update
+ // that is in progress.
+ this._hiding = true;
+
+ // Remove ourself as a download listener so that we don't continue to be
+ // fed progress and state notifications after the UI we're updating has
+ // gone away.
+ this.cleanUp();
+
+ var aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ var um = CoC["@mozilla.org/updates/update-manager;1"].
+ getService(CoI.nsIUpdateManager);
+ um.activeUpdate = gUpdates.update;
+
+ // If the download was paused by the user, ask the user if they want to
+ // have the update resume in the background.
+ var downloadInBackground = true;
+ if (this._paused) {
+ var title = gUpdates.getAUSString("resumePausedAfterCloseTitle");
+ var message = gUpdates.getAUSString("resumePausedAfterCloseMsg",
+ [gUpdates.brandName]);
+ var ps = Services.prompt;
+ var flags = ps.STD_YES_NO_BUTTONS;
+ // Focus the software update wizard before prompting. This will raise
+ // the software update wizard if it is minimized making it more obvious
+ // what the prompt is for and will solve the problem of windows
+ // obscuring the prompt. See bug #350299 for more details.
+ window.focus();
+ var rv = ps.confirmEx(window, title, message, flags, null, null, null,
+ null, { });
+ if (rv == CoI.nsIPromptService.BUTTON_POS_0)
+ downloadInBackground = false;
+ }
+ if (downloadInBackground) {
+ // Continue download in the background at full speed.
+ LOG("gDownloadingPage", "onHide - continuing download in background " +
+ "at full speed");
+ aus.downloadUpdate(gUpdates.update, false);
+ }
+ gUpdates.wiz.cancel();
+ },
+
+ /**
+ * When the data transfer begins
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ */
+ onStartRequest: function(request, context) {
+ // This !paused test is necessary because onStartRequest may fire after
+ // the download was paused (for those speedy clickers...)
+ if (this._paused)
+ return;
+
+ if (this._downloadProgress.mode != "undetermined")
+ this._downloadProgress.mode = "undetermined";
+ this._setStatus(this._label_downloadStatus);
+ },
+
+ /**
+ * When new data has been downloaded
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param progress
+ * The current number of bytes transferred
+ * @param maxProgress
+ * The total number of bytes that must be transferred
+ */
+ onProgress: function(request, context, progress, maxProgress) {
+ let status = this._updateDownloadStatus(progress, maxProgress);
+ var currentProgress = Math.round(100 * (progress / maxProgress));
+
+ var p = gUpdates.update.selectedPatch;
+ p.QueryInterface(CoI.nsIWritablePropertyBag);
+ p.setProperty("progress", currentProgress);
+ p.setProperty("status", status);
+
+ // This !paused test is necessary because onProgress may fire after
+ // the download was paused (for those speedy clickers...)
+ if (this._paused)
+ return;
+
+ if (this._downloadProgress.mode != "normal")
+ this._downloadProgress.mode = "normal";
+ if (this._downloadProgress.value != currentProgress)
+ this._downloadProgress.value = currentProgress;
+ if (this._pauseButton.disabled)
+ this._pauseButton.disabled = false;
+
+ // If the update has completed downloading and the download status contains
+ // the original text return early to avoid an assertion in debug builds.
+ // Since the page will advance immmediately due to the update completing the
+ // download updating the status is not important.
+ // nsTextFrame::GetTrimmedOffsets 'Can only call this on frames that have
+ // been reflowed'.
+ if (progress == maxProgress &&
+ this._downloadStatus.textContent == this._label_downloadStatus)
+ return;
+
+ this._setStatus(status);
+ },
+
+ /**
+ * When we have new status text
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param status
+ * A status code
+ * @param statusText
+ * Human readable version of |status|
+ */
+ onStatus: function(request, context, status, statusText) {
+ this._setStatus(statusText);
+ },
+
+ /**
+ * When data transfer ceases
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param status
+ * Status code containing the reason for the cessation.
+ */
+ onStopRequest: function(request, context, status) {
+ if (this._downloadProgress.mode != "normal")
+ this._downloadProgress.mode = "normal";
+
+ var u = gUpdates.update;
+ switch (status) {
+ case CoR.NS_ERROR_CORRUPTED_CONTENT:
+ case CoR.NS_ERROR_UNEXPECTED:
+ if (u.selectedPatch.state == STATE_DOWNLOAD_FAILED &&
+ (u.isCompleteUpdate || u.patchCount != 2)) {
+ // Verification error of complete patch, informational text is held in
+ // the update object.
+ this.cleanUp();
+ gUpdates.wiz.goTo("errors");
+ break;
+ }
+ // Verification failed for a partial patch, complete patch is now
+ // downloading so return early and do NOT remove the download listener!
+
+ // Reset the progress meter to "undertermined" mode so that we don't
+ // show old progress for the new download of the "complete" patch.
+ this._downloadProgress.mode = "undetermined";
+ this._pauseButton.disabled = true;
+ document.getElementById("verificationFailed").hidden = false;
+ break;
+ case CoR.NS_BINDING_ABORTED:
+ LOG("gDownloadingPage", "onStopRequest - pausing download");
+ // Do not remove UI listener since the user may resume downloading again.
+ break;
+ case CoR.NS_OK:
+ LOG("gDownloadingPage", "onStopRequest - patch verification succeeded");
+ // If the background update pref is set, we should wait until the update
+ // is actually staged in the background.
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (aus.canStageUpdates) {
+ this._setUpdateApplying();
+ } else {
+ this.cleanUp();
+ gUpdates.wiz.goTo("finished");
+ }
+ break;
+ default:
+ LOG("gDownloadingPage", "onStopRequest - transfer failed");
+ // Some kind of transfer error, die.
+ this.cleanUp();
+ gUpdates.wiz.goTo("errors");
+ break;
+ }
+ },
+
+ /**
+ * See nsIObserver.idl
+ */
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "update-staged") {
+ if (aData == STATE_DOWNLOADING) {
+ // We've fallen back to downloding the full update because the
+ // partial update failed to get staged in the background.
+ this._setStatus("downloading");
+ return;
+ }
+ this.cleanUp();
+ if (aData == STATE_APPLIED ||
+ aData == STATE_APPLIED_SERVICE ||
+ aData == STATE_PENDING ||
+ aData == STATE_PENDING_SERVICE ||
+ aData == STATE_PENDING_ELEVATE) {
+ // If the update is successfully applied, or if the updater has
+ // fallen back to non-staged updates, go to the finish page.
+ gUpdates.wiz.goTo("finished");
+ } else {
+ gUpdates.wiz.goTo("errors");
+ }
+ }
+ },
+
+ /**
+ * See nsISupports.idl
+ */
+ QueryInterface: function(iid) {
+ if (!iid.equals(CoI.nsIRequestObserver) &&
+ !iid.equals(CoI.nsIProgressEventSink) &&
+ !iid.equals(CoI.nsIObserver) &&
+ !iid.equals(CoI.nsISupports))
+ throw CoR.NS_ERROR_NO_INTERFACE;
+ return this;
+ }
+};
+
+/**
+ * The "There was an error during the update" page.
+ */
+var gErrorsPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ gUpdates.setButtons(null, null, "okButton", true);
+ gUpdates.wiz.getButton("finish").focus();
+
+ var statusText = gUpdates.update.statusText;
+ LOG("gErrorsPage", "onPageShow - update.statusText: " + statusText);
+
+ var errorReason = document.getElementById("errorReason");
+ errorReason.value = statusText;
+ var manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL);
+ var errorLinkLabel = document.getElementById("errorLinkLabel");
+ errorLinkLabel.value = manualURL;
+ errorLinkLabel.setAttribute("url", manualURL);
+ }
+};
+
+/**
+ * The page shown when there is a background check or a certificate attribute
+ * error.
+ */
+var gErrorExtraPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ gUpdates.setButtons(null, null, "okButton", true);
+ gUpdates.wiz.getButton("finish").focus();
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
+ }
+
+ document.getElementById("genericBackgroundErrorLabel").hidden = false;
+ let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL);
+ let errorLinkLabel = document.getElementById("errorExtraLinkLabel");
+ errorLinkLabel.value = manualURL;
+ errorLinkLabel.setAttribute("url", manualURL);
+ }
+};
+
+/**
+ * The "There was an error applying a partial patch" page.
+ */
+var gErrorPatchingPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ gUpdates.setButtons(null, null, "okButton", true);
+ },
+
+ onWizardNext: function() {
+ switch (gUpdates.update.selectedPatch.state) {
+ case STATE_APPLIED:
+ case STATE_APPLIED_SERVICE:
+ gUpdates.wiz.goTo("finished");
+ break;
+ case STATE_PENDING:
+ case STATE_PENDING_SERVICE:
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (!aus.canStageUpdates) {
+ gUpdates.wiz.goTo("finished");
+ break;
+ }
+ // intentional fallthrough
+ case STATE_DOWNLOADING:
+ gUpdates.wiz.goTo("downloading");
+ break;
+ case STATE_DOWNLOAD_FAILED:
+ gUpdates.wiz.goTo("errors");
+ break;
+ }
+ }
+};
+
+/**
+ * The "Update has been downloaded" page. Shows information about what
+ * was downloaded.
+ */
+var gFinishedPage = {
+ /**
+ * Initialize
+ */
+ onPageShow: function() {
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (aus.elevationRequired) {
+ LOG("gFinishedPage", "elevationRequired");
+ gUpdates.setButtons("restartLaterButton", "noThanksButton",
+ "restartNowButton", true);
+ } else {
+ LOG("gFinishedPage", "not elevationRequired");
+ gUpdates.setButtons("restartLaterButton", null, "restartNowButton",
+ true);
+ }
+ gUpdates.wiz.getButton("finish").focus();
+ },
+
+ /**
+ * Initialize the Wizard Page for a Background Source Event
+ */
+ onPageShowBackground: function() {
+ this.onPageShow();
+ let updateFinishedName = document.getElementById("updateFinishedName");
+ updateFinishedName.value = gUpdates.update.name;
+
+ let link = document.getElementById("finishedBackgroundLink");
+ if (gUpdates.update.detailsURL) {
+ link.setAttribute("url", gUpdates.update.detailsURL);
+ // The details link is stealing focus so it is disabled by default and
+ // should only be enabled after onPageShow has been called.
+ link.disabled = false;
+ } else {
+ link.hidden = true;
+ }
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (aus.elevationRequired) {
+ let more = document.getElementById("finishedBackgroundMore");
+ more.setAttribute("hidden", "true");
+ let moreElevated =
+ document.getElementById("finishedBackgroundMoreElevated");
+ moreElevated.setAttribute("hidden", "false");
+ let moreElevatedLink =
+ document.getElementById("finishedBackgroundMoreElevatedLink");
+ moreElevatedLink.setAttribute("hidden", "false");
+ let moreElevatedLinkLabel =
+ document.getElementById("finishedBackgroundMoreElevatedLinkLabel");
+ let manualURL = Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_MANUAL);
+ moreElevatedLinkLabel.value = manualURL;
+ moreElevatedLinkLabel.setAttribute("url", manualURL);
+ moreElevatedLinkLabel.setAttribute("hidden", "false");
+ }
+
+ if (getPref("getBoolPref", PREF_APP_UPDATE_TEST_LOOP, false)) {
+ setTimeout(function () { gUpdates.wiz.getButton("finish").click(); },
+ UPDATE_TEST_LOOP_INTERVAL);
+ }
+ },
+
+ /**
+ * Called when the wizard finishes, i.e. the "Restart Now" button is
+ * clicked.
+ */
+ onWizardFinish: function() {
+ // Do the restart
+ LOG("gFinishedPage", "onWizardFinish - restarting the application");
+
+ let aus = CoC["@mozilla.org/updates/update-service;1"].
+ getService(CoI.nsIApplicationUpdateService);
+ if (aus.elevationRequired) {
+ let um = CoC["@mozilla.org/updates/update-manager;1"].
+ getService(CoI.nsIUpdateManager);
+ if (um) {
+ um.elevationOptedIn();
+ }
+ }
+
+ // disable the "finish" (Restart) and "extra1" (Later) buttons
+ // because the Software Update wizard is still up at the point,
+ // and will remain up until we return and we close the
+ // window with a |window.close()| in wizard.xml
+ // (it was the firing the "wizardfinish" event that got us here.)
+ // This prevents the user from switching back
+ // to the Software Update dialog and clicking "Restart" or "Later"
+ // when dealing with the "confirm close" prompts.
+ // See bug #350299 for more details.
+ gUpdates.wiz.getButton("finish").disabled = true;
+ gUpdates.wiz.getButton("extra1").disabled = true;
+
+ // Notify all windows that an application quit has been requested.
+ var cancelQuit = CoC["@mozilla.org/supports-PRBool;1"].
+ createInstance(CoI.nsISupportsPRBool);
+ Services.obs.notifyObservers(cancelQuit, "quit-application-requested",
+ "restart");
+
+ // Something aborted the quit process.
+ if (cancelQuit.data)
+ return;
+
+ // If already in safe mode restart in safe mode (bug 327119)
+ if (Services.appinfo.inSafeMode) {
+ let env = CoC["@mozilla.org/process/environment;1"].
+ getService(CoI.nsIEnvironment);
+ env.set("MOZ_SAFE_MODE_RESTART", "1");
+ }
+
+ // Restart the application
+ CoC["@mozilla.org/toolkit/app-startup;1"].getService(CoI.nsIAppStartup).
+ quit(CoI.nsIAppStartup.eAttemptQuit | CoI.nsIAppStartup.eRestart);
+ },
+
+ /**
+ * When the user clicks the "Restart Later" instead of the Restart Now" button
+ * in the wizard after an update has been downloaded.
+ */
+ onExtra1: function() {
+ gUpdates.wiz.cancel();
+ },
+
+ /**
+ * When elevation is required and the user clicks "No Thanks" in the wizard.
+ */
+ onExtra2: Task.async(function*() {
+ Services.obs.notifyObservers(null, "update-canceled", null);
+ let um = CoC["@mozilla.org/updates/update-manager;1"].
+ getService(CoI.nsIUpdateManager);
+ um.cleanupActiveUpdate();
+ gUpdates.never();
+ gUpdates.wiz.cancel();
+ }),
+};
+
+/**
+ * Callback for the Update Prompt to set the current page if an Update Wizard
+ * window is already found to be open.
+ * @param pageid
+ * The ID of the page to switch to
+ */
+function setCurrentPage(pageid) {
+ gUpdates.wiz.currentPage = document.getElementById(pageid);
+}
diff --git a/toolkit/mozapps/update/content/updates.xml b/toolkit/mozapps/update/content/updates.xml
new file mode 100644
index 000000000..9e8d4e936
--- /dev/null
+++ b/toolkit/mozapps/update/content/updates.xml
@@ -0,0 +1,83 @@
+<?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 bindings SYSTEM "chrome://mozapps/locale/update/updates.dtd">
+
+<bindings id="updatesBindings"
+ 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">
+
+ <binding id="updateheader" extends="chrome://global/content/bindings/wizard.xml#wizard-header">
+ <resources>
+ <stylesheet src="chrome://global/skin/wizard.css"/>
+ </resources>
+ <content>
+ <xul:hbox class="wizard-header update-header" flex="1">
+ <xul:vbox class="wizard-header-box-1">
+ <xul:vbox class="wizard-header-box-text">
+ <xul:label class="wizard-header-label" xbl:inherits="value=label"/>
+ </xul:vbox>
+ </xul:vbox>
+ </xul:hbox>
+ </content>
+ </binding>
+
+ <binding id="update" extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
+ <content>
+ <xul:hbox>
+ <xul:label class="update-name" xbl:inherits="value=name" flex="1" crop="right"/>
+ <xul:label xbl:inherits="href=detailsURL,hidden=hideDetailsURL" class="text-link"
+ value="&update.details.label;"/>
+ </xul:hbox>
+ <xul:label class="update-type" xbl:inherits="value=type"/>
+ <xul:grid>
+ <xul:columns>
+ <xul:column class="update-label-column"/>
+ <xul:column flex="1"/>
+ </xul:columns>
+ <xul:rows>
+ <xul:row>
+ <xul:label class="update-installedOn-label">&update.installedOn.label;</xul:label>
+ <xul:label class="update-installedOn-value" xbl:inherits="value=installDate" flex="1" crop="right"/>
+ </xul:row>
+ <xul:row>
+ <xul:label class="update-status-label">&update.status.label;</xul:label>
+ <xul:description class="update-status-value" flex="1"/>
+ </xul:row>
+ </xul:rows>
+ </xul:grid>
+ </content>
+ <implementation>
+ <property name="name"
+ onget="return this.getAttribute('name');"
+ onset="this.setAttribute('name', val); return val;"/>
+ <property name="detailsURL"
+ onget="return this.getAttribute('detailsURL');"
+ onset="this.setAttribute('detailsURL', val); return val;"/>
+ <property name="installDate"
+ onget="return this.getAttribute('installDate');"
+ onset="this.setAttribute('installDate', val); return val;"/>
+ <property name="type"
+ onget="return this.getAttribute('type');"
+ onset="this.setAttribute('type', val); return val;"/>
+ <property name="hideDetailsURL"
+ onget="return this.getAttribute('hideDetailsURL');"
+ onset="this.setAttribute('hideDetailsURL', val); return val;"/>
+ <property name="status"
+ onget="return this.getAttribute('status');">
+ <setter><![CDATA[
+ this.setAttribute("status", val);
+ var field = document.getAnonymousElementByAttribute(this, "class", "update-status-value");
+ while (field.hasChildNodes())
+ field.removeChild(field.firstChild);
+ field.appendChild(document.createTextNode(val));
+ return val;
+ ]]></setter>
+ </property>
+ </implementation>
+ </binding>
+</bindings>
diff --git a/toolkit/mozapps/update/content/updates.xul b/toolkit/mozapps/update/content/updates.xul
new file mode 100644
index 000000000..e4cdc7a49
--- /dev/null
+++ b/toolkit/mozapps/update/content/updates.xul
@@ -0,0 +1,206 @@
+<?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/update/updates.css"?>
+<?xml-stylesheet href="chrome://mozapps/skin/update/updates.css"?>
+
+<!DOCTYPE wizard [
+<!ENTITY % updateDTD SYSTEM "chrome://mozapps/locale/update/updates.dtd">
+<!ENTITY % brandDTD SYSTEM "chrome://branding/locale/brand.dtd">
+%updateDTD;
+%brandDTD;
+]>
+
+<wizard id="updates"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ title="&updateWizard.title;"
+ windowtype="Update:Wizard"
+ style="width: auto; height: auto"
+ onwizardfinish="gUpdates.onWizardFinish();"
+ onwizardcancel="gUpdates.onWizardCancel();"
+ onwizardnext="gUpdates.onWizardNext();"
+ onload="gUpdates.onLoad();"
+ onunload="gUpdates.onUnload();">
+
+ <script type="application/javascript" src="chrome://global/content/contentAreaUtils.js"/>
+ <script type="application/javascript" src="chrome://mozapps/content/update/updates.js"/>
+
+ <stringbundleset id="updateSet">
+ <stringbundle id="brandStrings" src="chrome://branding/locale/brand.properties"/>
+ <stringbundle id="updateStrings" src="chrome://mozapps/locale/update/updates.properties"/>
+ </stringbundleset>
+
+ <wizardpage id="dummy" pageid="dummy" firstpage="true"/>
+
+ <wizardpage id="checking" pageid="checking" next="noupdatesfound"
+ object="gCheckingPage" onpageshow="gCheckingPage.onPageShow();">
+ <updateheader label="&checking.title;"/>
+ <vbox class="update-content" flex="1">
+ <label>&updateCheck.label;</label>
+ <separator class="thin"/>
+ <progressmeter id="checkingProgress" mode="undetermined"/>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="noupdatesfound" pageid="noupdatesfound"
+ object="gNoUpdatesPage" onpageshow="gNoUpdatesPage.onPageShow();">
+ <updateheader label="&noupdatesfound.title;"/>
+ <vbox class="update-content" flex="1">
+ <label id="noUpdatesAutoEnabled" hidden="true">&noupdatesautoenabled.intro;</label>
+ <label id="noUpdatesAutoDisabled" hidden="true">&noupdatesautodisabled.intro;</label>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="manualUpdate" pageid="manualUpdate" object="gManualUpdatePage"
+ onpageshow="gManualUpdatePage.onPageShow();">
+ <updateheader label="&manualUpdate.title;"/>
+ <vbox class="update-content" flex="1">
+ <label id="manualUpdateDesc">&manualUpdate.desc;</label>
+ <label id="manualUpdateSpaceDesc"
+ hidden="true">&manualUpdate.space.desc;</label>
+ <separator class="thin"/>
+ <label>&manualUpdateGetMsg.label;</label>
+ <hbox>
+ <label class="text-link" id="manualUpdateLinkLabel" value=""
+ onclick="openUpdateURL(event);"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="unsupported" pageid="unsupported"
+ object="gUnsupportedPage"
+ onpageshow="gUnsupportedPage.onPageShow();">
+ <updateheader label="&unsupported.title;"/>
+ <vbox class="update-content" flex="1">
+ <description flex="1">&unsupported.label;
+ <label id="unsupportedLinkLabel" class="text-link inline-link" onclick="openUpdateURL(event);">
+ &unsupportedLink.label;
+ </label>
+ </description>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="updatesfoundbasic" pageid="updatesfoundbasic"
+ object="gUpdatesFoundBasicPage" next="downloading"
+ onpageshow="gUpdatesFoundBasicPage.onPageShow();"
+ onextra1="gUpdatesFoundBasicPage.onExtra1();"
+ onextra2="gUpdatesFoundBasicPage.onExtra2();">
+ <updateheader id="updatesFoundBasicHeader" label=""/>
+ <vbox class="update-content" flex="1">
+ <label id="updatesFoundInto"/>
+ <separator class="thin"/>
+ <label id="updateName" crop="right" value=""/>
+ <separator id="updateNameSep" class="thin"/>
+ <label id="upgradeEvangelism">&evangelism.desc;</label>
+ <separator id="upgradeEvangelismSep" flex="1"/>
+ <vbox flex="1">
+ <hbox id="moreInfoURL">
+ <label class="text-link" id="updateMoreInfoURL"
+ value="&clickHere.label;" onclick="openUpdateURL(event);"/>
+ </hbox>
+ </vbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="downloading" pageid="downloading"
+ object="gDownloadingPage" onextra1="gDownloadingPage.onHide();"
+ onpageshow="gDownloadingPage.onPageShow();">
+ <updateheader label="&downloadPage.title;"/>
+ <vbox class="update-content" flex="1">
+ <hbox id="downloadStatusProgress">
+ <progressmeter id="downloadProgress" mode="undetermined" flex="1"/>
+ <button id="pauseButton" oncommand="gDownloadingPage.onPause();"
+ paused="false"/>
+ </hbox>
+ <separator class="thin"/>
+ <hbox id="downloadStatusLine">
+ <label id="downloadStatus" flex="1">&connecting.label;</label>
+ </hbox>
+ <separator/>
+ <hbox id="verificationFailed" align="start" hidden="true">
+ <image id="verificationFailedIcon"/>
+ <label flex="1">&verificationFailedText.label;</label>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="errors" pageid="errors" object="gErrorsPage"
+ onpageshow="gErrorsPage.onPageShow();">
+ <updateheader label="&error.title;"/>
+ <vbox class="update-content" flex="1">
+ <label id="errorIntro">&error.label;</label>
+ <separator/>
+ <textbox class="plain" readonly="true" id="errorReason" multiline="true"
+ rows="3"/>
+ <separator/>
+ <label id="errorManual">&errorManual.label;</label>
+ <hbox>
+ <label class="text-link" id="errorLinkLabel" value=""
+ onclick="openUpdateURL(event);"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="errorextra" pageid="errorextra"
+ object="gErrorExtraPage"
+ onpageshow="gErrorExtraPage.onPageShow();">
+ <updateheader label="&error.title;"/>
+ <vbox class="update-content" flex="1">
+ <label id="bgErrorLabel">&genericBackgroundError.label;</label>
+ <hbox>
+ <label id="errorExtraLinkLabel" class="text-link"
+ value="" onclick="openUpdateURL(event);"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="errorpatching" pageid="errorpatching" next="downloading"
+ object="gErrorPatchingPage"
+ onpageshow="gErrorPatchingPage.onPageShow();">
+ <updateheader label="&error.title;"/>
+ <vbox class="update-content" flex="1">
+ <label>&errorpatching.intro;</label>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="finished" pageid="finished" object="gFinishedPage"
+ onpageshow="gFinishedPage.onPageShow();"
+ onextra1="gFinishedPage.onExtra1()">
+ <updateheader label="&finishedPage.title;"/>
+ <vbox class="update-content" flex="1">
+ <label>&finishedPage.text;</label>
+ </vbox>
+ </wizardpage>
+
+ <wizardpage id="finishedBackground" pageid="finishedBackground"
+ object="gFinishedPage" onextra1="gFinishedPage.onExtra1()"
+ onextra2="gFinishedPage.onExtra2()"
+ onpageshow="gFinishedPage.onPageShowBackground();">
+ <updateheader label="&finishedPage.title;"/>
+ <vbox class="update-content" flex="1">
+ <label>&finishedBackgroundPage.text;</label>
+ <separator/>
+ <hbox align="center">
+ <label>&finishedBackground.name;</label>
+ <label id="updateFinishedName" flex="1" crop="right" value=""/>
+ <label id="finishedBackgroundLink" class="text-link" disabled="true"
+ value="&details.link;" onclick="openUpdateURL(event);"/>
+ </hbox>
+ <spacer flex="1"/>
+ <label id="finishedBackgroundMore">&finishedBackground.more;</label>
+ <label id="finishedBackgroundMoreElevated"
+ hidden="true">&finishedBackground.moreElevated;</label>
+ <label id="finishedBackgroundMoreElevatedLink"
+ hidden="true">&errorManual.label;</label>
+ <hbox>
+ <label class="text-link" id="finishedBackgroundMoreElevatedLinkLabel"
+ value="" onclick="openUpdateURL(event);" hidden="true"/>
+ </hbox>
+ </vbox>
+ </wizardpage>
+
+</wizard>
diff --git a/toolkit/mozapps/update/jar.mn b/toolkit/mozapps/update/jar.mn
new file mode 100644
index 000000000..9bb5d2d05
--- /dev/null
+++ b/toolkit/mozapps/update/jar.mn
@@ -0,0 +1,12 @@
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+toolkit.jar:
+% content mozapps %content/mozapps/
+ content/mozapps/update/history.xul (content/history.xul)
+ content/mozapps/update/history.js (content/history.js)
+ content/mozapps/update/updates.css (content/updates.css)
+ content/mozapps/update/updates.js (content/updates.js)
+ content/mozapps/update/updates.xml (content/updates.xml)
+ content/mozapps/update/updates.xul (content/updates.xul)
diff --git a/toolkit/mozapps/update/moz.build b/toolkit/mozapps/update/moz.build
new file mode 100644
index 000000000..78a6996b7
--- /dev/null
+++ b/toolkit/mozapps/update/moz.build
@@ -0,0 +1,33 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+XPIDL_MODULE = 'update'
+
+DIRS += [
+ 'common',
+ 'updater',
+]
+
+XPIDL_SOURCES += [
+ 'nsIUpdateService.idl',
+]
+
+TEST_DIRS += ['tests']
+
+EXTRA_COMPONENTS += [
+ 'nsUpdateService.js',
+ 'nsUpdateService.manifest',
+ 'nsUpdateServiceStub.js',
+]
+
+EXTRA_JS_MODULES += [
+ 'UpdateTelemetry.jsm',
+]
+
+JAR_MANIFESTS += ['jar.mn']
+
+with Files('**'):
+ BUG_COMPONENT = ('Toolkit', 'Application Update')
diff --git a/toolkit/mozapps/update/nsIUpdateService.idl b/toolkit/mozapps/update/nsIUpdateService.idl
new file mode 100644
index 000000000..817df5a67
--- /dev/null
+++ b/toolkit/mozapps/update/nsIUpdateService.idl
@@ -0,0 +1,575 @@
+/* -*- Mode: IDL; tab-width: 2; 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 "nsISupports.idl"
+
+interface nsIDOMDocument;
+interface nsIDOMElement;
+interface nsIDOMWindow;
+interface nsIRequest;
+interface nsIRequestObserver;
+interface nsISimpleEnumerator;
+interface nsIFile;
+
+/**
+ * An interface that describes an object representing a patch file that can
+ * be downloaded and applied to a version of this application so that it
+ * can be updated.
+ */
+[scriptable, uuid(dc8fb8a9-3a53-4031-9469-2a5197ea30e7)]
+interface nsIUpdatePatch : nsISupports
+{
+ /**
+ * The type of this patch:
+ * "partial" A binary difference between two application versions
+ * "complete" A complete patch containing all of the replacement files
+ * to update to the new version
+ */
+ attribute AString type;
+
+ /**
+ * The URL this patch was being downloaded from
+ */
+ attribute AString URL;
+
+ /**
+ * The final URL this patch was being downloaded from
+ */
+ attribute AString finalURL;
+
+ /**
+ * The hash function to use when determining this file's integrity
+ */
+ attribute AString hashFunction;
+
+ /**
+ * The value of the hash function named above that should be computed if
+ * this file is not corrupt.
+ */
+ attribute AString hashValue;
+
+ /**
+ * The size of this file, in bytes.
+ */
+ attribute unsigned long size;
+
+ /**
+ * The state of this patch
+ */
+ attribute AString state;
+
+ /**
+ * true if this patch is currently selected as the patch to be downloaded and
+ * installed for this update transaction, false if another patch from this
+ * update has been selected.
+ */
+ attribute boolean selected;
+
+ /**
+ * Serializes this patch object into a DOM Element
+ * @param updates
+ * The document to serialize into
+ * @returns The DOM Element created by the serialization process
+ */
+ nsIDOMElement serialize(in nsIDOMDocument updates);
+};
+
+/**
+ * An interface that describes an object representing an available update to
+ * the current application - this update may have several available patches
+ * from which one must be selected to download and install, for example we
+ * might select a binary difference patch first and attempt to apply that,
+ * then if the application process fails fall back to downloading a complete
+ * file-replace patch. This object also contains information about the update
+ * that the front end and other application services can use to learn more
+ * about what is going on.
+ */
+[scriptable, uuid(e094c045-f4ff-41fd-92da-cd2effd2c7c9)]
+interface nsIUpdate : nsISupports
+{
+ /**
+ * The type of update:
+ * "major" A major new version of the Application
+ * "minor" A minor update to the Application (e.g. security update)
+ */
+ attribute AString type;
+
+ /**
+ * The name of the update, or "<Application Name> <Update Version>"
+ */
+ attribute AString name;
+
+ /**
+ * The string to display in the user interface for the version. If you want
+ * a real version number use appVersion.
+ */
+ attribute AString displayVersion;
+
+ /**
+ * The Application version of this update.
+ */
+ attribute AString appVersion;
+
+ /**
+ * The Application version prior to the application being updated.
+ */
+ attribute AString previousAppVersion;
+
+ /**
+ * The Build ID of this update. Used to determine a particular build, down
+ * to the hour, minute and second of its creation. This allows the system
+ * to differentiate between several nightly builds with the same |version|
+ * for example.
+ */
+ attribute AString buildID;
+
+ /**
+ * The URL to a page which offers details about the content of this
+ * update. Ideally, this page is not the release notes but some other page
+ * that summarizes the differences between this update and the previous,
+ * which also links to the release notes.
+ */
+ attribute AString detailsURL;
+
+ /**
+ * The URL to the Update Service that supplied this update.
+ */
+ attribute AString serviceURL;
+
+ /**
+ * The channel used to retrieve this update from the Update Service.
+ */
+ attribute AString channel;
+
+ /**
+ * Whether to show the update prompt which requires user confirmation when an
+ * update is found during a background update check. This overrides the
+ * default setting to download the update in the background.
+ */
+ attribute boolean showPrompt;
+
+ /**
+ * Whether to show the "No Thanks" button in the update prompt. This allows
+ * the user to never receive a notification for that specific update version
+ * again.
+ */
+ attribute boolean showNeverForVersion;
+
+ /**
+ * Whether the update is no longer supported on this system.
+ */
+ attribute boolean unsupported;
+
+ /**
+ * Allows overriding the default amount of time in seconds before prompting the
+ * user to apply an update. If not specified, the value of
+ * app.update.promptWaitTime will be used.
+ */
+ attribute long long promptWaitTime;
+
+ /**
+ * Whether or not the update being downloaded is a complete replacement of
+ * the user's existing installation or a patch representing the difference
+ * between the new version and the previous version.
+ */
+ attribute boolean isCompleteUpdate;
+
+ /**
+ * Whether or not the update is a security update or not. If this is true,
+ * then we present more serious sounding user interface messages to the
+ * user.
+ */
+ attribute boolean isSecurityUpdate;
+
+ /**
+ * Whether or not the update being downloaded is an OS update. This is
+ * generally only possible in Gonk right now.
+ */
+ attribute boolean isOSUpdate;
+
+ /**
+ * When the update was installed.
+ */
+ attribute long long installDate;
+
+ /**
+ * A message associated with this update, if any.
+ */
+ attribute AString statusText;
+
+ /**
+ * The currently selected patch for this update.
+ */
+ readonly attribute nsIUpdatePatch selectedPatch;
+
+ /**
+ * The state of the selected patch:
+ * "downloading" The update is being downloaded.
+ * "pending" The update is ready to be applied.
+ * "pending-service" The update is ready to be applied with the service.
+ * "pending-elevate" The update is ready to be applied but requires elevation.
+ * "applying" The update is being applied.
+ * "applied" The update is ready to be switched to.
+ * "applied-os" The update is OS update and to be installed.
+ * "applied-service" The update is ready to be switched to with the service.
+ * "succeeded" The update was successfully applied.
+ * "download-failed" The update failed to be downloaded.
+ * "failed" The update failed to be applied.
+ */
+ attribute AString state;
+
+ /**
+ * A numeric error code that conveys additional information about the state
+ * of a failed update or failed certificate attribute check during an update
+ * check. If the update is not in the "failed" state or the certificate
+ * attribute check has not failed the value is zero.
+ *
+ * TODO: Define typical error codes (for now, see updater/errors.h and the
+ * CERT_ATTR_CHECK_FAILED_* values in nsUpdateService.js)
+ */
+ attribute long errorCode;
+
+ /**
+ * Whether an elevation failure has been encountered for this update.
+ */
+ attribute boolean elevationFailure;
+
+ /**
+ * The number of patches supplied by this update.
+ */
+ readonly attribute unsigned long patchCount;
+
+ /**
+ * Retrieves a patch.
+ * @param index
+ * The index of the patch to retrieve.
+ * @returns The nsIUpdatePatch at the specified index.
+ */
+ nsIUpdatePatch getPatchAt(in unsigned long index);
+
+ /**
+ * Serializes this update object into a DOM Element
+ * @param updates
+ * The document to serialize into
+ * @returns The DOM Element created by the serialization process
+ */
+ nsIDOMElement serialize(in nsIDOMDocument updates);
+};
+
+/**
+ * An interface describing an object that listens to the progress of an update
+ * check operation. This object is notified as the check continues, finishes
+ * and if it has an error.
+ */
+[scriptable, uuid(4aa2b4bb-39ea-407b-98ff-89f19134d4c0)]
+interface nsIUpdateCheckListener : nsISupports
+{
+ /**
+ * The update check was completed.
+ * @param request
+ * The XMLHttpRequest handling the update check.
+ * @param updates
+ * An array of nsIUpdate objects listing available updates.
+ * @param updateCount
+ * The size of the |updates| array.
+ */
+ void onCheckComplete(in jsval request,
+ [array, size_is(updateCount)] in nsIUpdate updates,
+ in unsigned long updateCount);
+
+ /**
+ * An error occurred while loading the remote update service file.
+ * @param request
+ * The XMLHttpRequest handling the update check.
+ * @param update
+ * A nsIUpdate object that contains details about the
+ * error in its |statusText| property.
+ */
+ void onError(in jsval request,
+ in nsIUpdate update);
+};
+
+/**
+ * An interface describing an object that knows how to check for updates.
+ */
+[scriptable, uuid(877ace25-8bc5-452a-8586-9c1cf2871994)]
+interface nsIUpdateChecker : nsISupports
+{
+ /**
+ * Checks for available updates, notifying a listener of the results.
+ * @param listener
+ * An object implementing nsIUpdateCheckListener which is notified
+ * of the results of an update check.
+ * @param force
+ * Forces the checker to check for updates, regardless of the
+ * current value of the user's update settings. This is used by
+ * any piece of UI that offers the user the imperative option to
+ * check for updates now, regardless of their update settings.
+ * force will not work if the system administrator has locked
+ * the app.update.enabled preference.
+ */
+ void checkForUpdates(in nsIUpdateCheckListener listener, in boolean force);
+
+ /**
+ * Constants for the |stopChecking| function that tell the Checker how long
+ * to stop checking:
+ *
+ * CURRENT_CHECK: Stops the current (active) check only
+ * CURRENT_SESSION: Stops all checking for the current session
+ * ANY_CHECKS: Stops all checking, any session from now on
+ * (disables update checking preferences)
+ */
+ const unsigned short CURRENT_CHECK = 1;
+ const unsigned short CURRENT_SESSION = 2;
+ const unsigned short ANY_CHECKS = 3;
+
+ /**
+ * Ends any pending update check.
+ * @param duration
+ * A value representing the set of checks to stop doing.
+ */
+ void stopChecking(in unsigned short duration);
+};
+
+/**
+ * An interface describing a global application service that handles performing
+ * background update checks and provides utilities for selecting and
+ * downloading update patches.
+ */
+[scriptable, uuid(1107d207-a263-403a-b268-05772ec10757)]
+interface nsIApplicationUpdateService : nsISupports
+{
+ /**
+ * Checks for available updates in the background using the listener provided
+ * by the application update service for background checks.
+ */
+ void checkForBackgroundUpdates();
+
+ /**
+ * The Update Checker used for background update checking.
+ */
+ readonly attribute nsIUpdateChecker backgroundChecker;
+
+ /**
+ * Selects the best update to install from a list of available updates.
+ * @param updates
+ * An array of updates that are available
+ * @param updateCount
+ * The length of the |updates| array
+ */
+ nsIUpdate selectUpdate([array, size_is(updateCount)] in nsIUpdate updates,
+ in unsigned long updateCount);
+
+ /**
+ * Adds a listener that receives progress and state information about the
+ * update that is currently being downloaded, e.g. to update a user
+ * interface.
+ * @param listener
+ * An object implementing nsIRequestObserver and optionally
+ * nsIProgressEventSink that is to be notified of state and
+ * progress information as the update is downloaded.
+ */
+ void addDownloadListener(in nsIRequestObserver listener);
+
+ /**
+ * Removes a listener that is receiving progress and state information
+ * about the update that is currently being downloaded.
+ * @param listener
+ * The listener object to remove.
+ */
+ void removeDownloadListener(in nsIRequestObserver listener);
+
+ /**
+ *
+ */
+ AString downloadUpdate(in nsIUpdate update, in boolean background);
+
+ /**
+ * Apply the OS update which has been downloaded and staged as applied.
+ * @param update
+ * The update has been downloaded and staged as applied.
+ * @throws if the update object is not an OS update.
+ */
+ void applyOsUpdate(in nsIUpdate update);
+
+ /**
+ * Get the Active Updates directory
+ * @returns An nsIFile for the active updates directory.
+ */
+ nsIFile getUpdatesDirectory();
+
+ /**
+ * Pauses the active update download process
+ */
+ void pauseDownload();
+
+ /**
+ * Whether or not there is an download happening at the moment.
+ */
+ readonly attribute boolean isDownloading;
+
+ /**
+ * Whether or not the Update Service can check for updates. This is a function
+ * of whether or not application update is disabled by the application and the
+ * platform the application is running on.
+ */
+ readonly attribute boolean canCheckForUpdates;
+
+ /**
+ * Whether or not the installation requires elevation. Currently only
+ * implemented on OSX, returns false on other platforms.
+ */
+ readonly attribute boolean elevationRequired;
+
+ /**
+ * Whether or not the Update Service can download and install updates.
+ * On Windows, this is a function of whether or not the maintenance service
+ * is installed and enabled. On other systems, and as a fallback on Windows,
+ * this depends on whether the current user has write access to the install
+ * directory.
+ */
+ readonly attribute boolean canApplyUpdates;
+
+ /**
+ * Whether or not a different instance is handling updates of this
+ * installation. This currently only ever returns true on Windows
+ * when 2 instances of an application are open. Only one of the instances
+ * will actually handle updates for the installation.
+ */
+ readonly attribute boolean isOtherInstanceHandlingUpdates;
+
+ /**
+ * Whether the Update Service is able to stage updates.
+ */
+ readonly attribute boolean canStageUpdates;
+};
+
+/**
+ * An interface describing a component which handles the job of processing
+ * an update after it's been downloaded.
+ */
+[scriptable, uuid(74439497-d796-4915-8cef-3dfe43027e4d)]
+interface nsIUpdateProcessor : nsISupports
+{
+ /**
+ * Processes the update which has been downloaded.
+ * This happens without restarting the application.
+ * On Windows, this can also be used for switching to an applied
+ * update request.
+ * @param update The update being applied, or null if this is a switch
+ * to updated application request. Must be non-null on GONK.
+ */
+ void processUpdate(in nsIUpdate update);
+};
+
+/**
+ * An interface describing a global application service that maintains a list
+ * of updates previously performed as well as the current active update.
+ */
+[scriptable, uuid(0f1098e9-a447-4af9-b030-6f8f35c85f89)]
+interface nsIUpdateManager : nsISupports
+{
+ /**
+ * Gets the update at the specified index
+ * @param index
+ * The index within the updates array
+ * @returns The nsIUpdate object at the specified index
+ */
+ nsIUpdate getUpdateAt(in long index);
+
+ /**
+ * Gets the total number of updates in the history list.
+ */
+ readonly attribute long updateCount;
+
+ /**
+ * The active (current) update. The active update is not in the history list.
+ */
+ attribute nsIUpdate activeUpdate;
+
+ /**
+ * Saves all updates to disk.
+ */
+ void saveUpdates();
+
+ /**
+ * Refresh the update status based on the information in update.status.
+ */
+ void refreshUpdateStatus();
+
+ /**
+ * The user agreed to proceed with an elevated update and we are now
+ * permitted to show an elevation prompt.
+ */
+ void elevationOptedIn();
+
+ /**
+ * Clean up and remove the active update without applying it.
+ */
+ void cleanupActiveUpdate();
+};
+
+/**
+ * An interface describing an object that can show various kinds of Update
+ * notification UI to the user.
+ */
+[scriptable, uuid(cee3bd60-c564-42ff-a2bf-d442cb15f75c)]
+interface nsIUpdatePrompt : nsISupports
+{
+ /**
+ * Shows the application update checking user interface and checks if there
+ * is an update available.
+ */
+ void checkForUpdates();
+
+ /**
+ * Shows the application update available user interface advising that an
+ * update is available for download and install. If the app.update.silent
+ * preference is true or the user interface is already displayed the call will
+ * be a no-op.
+ * @param update
+ * The nsIUpdate object to be downloaded and installed
+ */
+ void showUpdateAvailable(in nsIUpdate update);
+
+ /**
+ * Shows the application update downloaded user interface advising that an
+ * update has now been downloaded and a restart is necessary to complete the
+ * update. If background is true (e.g. the download was not user initiated)
+ * and the app.update.silent preference is true the call will be a no-op.
+ * @param update
+ * The nsIUpdate object that was downloaded
+ * @param background
+ * Less obtrusive UI, starting with a non-modal notification alert
+ */
+ void showUpdateDownloaded(in nsIUpdate update,
+ [optional] in boolean background);
+
+ /**
+ * Shows the application update error user interface advising that an error
+ * occurred while checking for or applying an update. If the app.update.silent
+ * preference is true the call will be a no-op.
+ * @param update
+ * An nsIUpdate object representing the update that could not be
+ * installed. The nsIUpdate object will not be the actual update when
+ * the error occurred during an update check and will instead be an
+ * nsIUpdate object with the error information for the update check.
+ */
+ void showUpdateError(in nsIUpdate update);
+
+ /**
+ * Shows a list of all updates installed to date.
+ * @param parent
+ * An nsIDOMWindow to set as the parent for this window. Can be null.
+ */
+ void showUpdateHistory(in nsIDOMWindow parent);
+
+ /**
+ * Shows the application update downloaded user interface advising that an
+ * update, which requires elevation, has now been downloaded and a restart is
+ * necessary to complete the update.
+ */
+ void showUpdateElevationRequired();
+};
diff --git a/toolkit/mozapps/update/nsUpdateService.js b/toolkit/mozapps/update/nsUpdateService.js
new file mode 100644
index 000000000..10eb1d100
--- /dev/null
+++ b/toolkit/mozapps/update/nsUpdateService.js
@@ -0,0 +1,4567 @@
+/* -*- Mode: javascript; 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/. */
+
+'use strict';
+
+const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/FileUtils.jsm", this);
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/ctypes.jsm", this);
+Cu.import("resource://gre/modules/UpdateTelemetry.jsm", this);
+Cu.import("resource://gre/modules/AppConstants.jsm", this);
+Cu.importGlobalProperties(["XMLHttpRequest"]);
+
+const UPDATESERVICE_CID = Components.ID("{B3C290A6-3943-4B89-8BBE-C01EB7B3B311}");
+const UPDATESERVICE_CONTRACTID = "@mozilla.org/updates/update-service;1";
+
+const PREF_APP_UPDATE_ALTWINDOWTYPE = "app.update.altwindowtype";
+const PREF_APP_UPDATE_AUTO = "app.update.auto";
+const PREF_APP_UPDATE_BACKGROUNDINTERVAL = "app.update.download.backgroundInterval";
+const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
+const PREF_APP_UPDATE_CANCELATIONS_OSX = "app.update.cancelations.osx";
+const PREF_APP_UPDATE_CANCELATIONS_OSX_MAX = "app.update.cancelations.osx.max";
+const PREF_APP_UPDATE_ELEVATE_NEVER = "app.update.elevate.never";
+const PREF_APP_UPDATE_ELEVATE_VERSION = "app.update.elevate.version";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
+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 PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
+
+const URI_BRAND_PROPERTIES = "chrome://branding/locale/brand.properties";
+const URI_UPDATE_HISTORY_DIALOG = "chrome://mozapps/content/update/history.xul";
+const URI_UPDATE_NS = "http://www.mozilla.org/2005/app-update";
+const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
+const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties";
+
+const KEY_UPDROOT = "UpdRootD";
+const KEY_EXECUTABLE = "XREExeF";
+// Gonk only
+const KEY_UPDATE_ARCHIVE_DIR = "UpdArchD";
+
+const DIR_UPDATES = "updates";
+
+const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
+const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
+const FILE_LAST_UPDATE_LOG = "last-update.log";
+const FILE_UPDATES_XML = "updates.xml";
+const FILE_UPDATE_LINK = "update.link";
+const FILE_UPDATE_LOG = "update.log";
+const FILE_UPDATE_MAR = "update.mar";
+const FILE_UPDATE_STATUS = "update.status";
+const FILE_UPDATE_TEST = "update.test";
+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";
+
+// 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;
+const WRITE_ERROR_PATCH_FILE = 64;
+const WRITE_ERROR_APPLY_DIR_PATH = 65;
+const WRITE_ERROR_CALLBACK_PATH = 66;
+const WRITE_ERROR_FILE_ACCESS_DENIED = 67;
+const WRITE_ERROR_DIR_ACCESS_DENIED = 68;
+const WRITE_ERROR_DELETE_BACKUP = 69;
+const WRITE_ERROR_EXTRACT = 70;
+
+// Array of write errors to simplify checks for write errors
+const WRITE_ERRORS = [WRITE_ERROR,
+ WRITE_ERROR_ACCESS_DENIED,
+ WRITE_ERROR_CALLBACK_APP,
+ WRITE_ERROR_FILE_COPY,
+ WRITE_ERROR_DELETE_FILE,
+ WRITE_ERROR_OPEN_PATCH_FILE,
+ WRITE_ERROR_PATCH_FILE,
+ WRITE_ERROR_APPLY_DIR_PATH,
+ WRITE_ERROR_CALLBACK_PATH,
+ WRITE_ERROR_FILE_ACCESS_DENIED,
+ WRITE_ERROR_DIR_ACCESS_DENIED,
+ 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;
+const FOTA_UNKNOWN_ERROR = 81;
+const FOTA_FILE_OPERATION_ERROR = 82;
+const FOTA_RECOVERY_ERROR = 83;
+const INVALID_UPDATER_STATE_CODE = 98;
+const INVALID_UPDATER_STATUS_CODE = 99;
+
+// Custom update error codes
+const BACKGROUNDCHECK_MULTIPLE_FAILURES = 110;
+const NETWORK_ERROR_OFFLINE = 111;
+
+// Error codes should be < 1000. Errors above 1000 represent http status codes
+const HTTP_ERROR_OFFSET = 1000;
+
+const DOWNLOAD_CHUNK_SIZE = 300000; // bytes
+const DOWNLOAD_BACKGROUND_INTERVAL = 600; // seconds
+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;
+
+// The number of milliseconds to wait before retrying a connection error.
+const DEFAULT_SOCKET_RETRYTIMEOUT = 2000;
+
+// Default maximum number of elevation cancelations per update version before
+// giving up.
+const DEFAULT_CANCELATIONS_OSX_MAX = 3;
+
+// This maps app IDs to their respective notification topic which signals when
+// the application's user interface has been displayed.
+const APPID_TO_TOPIC = {
+ // Firefox
+ "{ec8030f7-c20a-464f-9b0e-13a3a9e97384}": "sessionstore-windows-restored",
+ // SeaMonkey
+ "{92650c4d-4b8e-4d2a-b7eb-24ecf4f6b63a}": "sessionstore-windows-restored",
+ // Fennec
+ "{aa3c5121-dab2-40e2-81ca-7ea25febc110}": "sessionstore-windows-restored",
+ // Thunderbird
+ "{3550f703-e582-4d05-9a08-453d09bdfdc6}": "mail-startup-done",
+ // Instantbird
+ "{33cb9019-c295-46dd-be21-8c4936574bee}": "xul-window-visible",
+};
+
+var gUpdateMutexHandle = null;
+
+// Gonk only
+var gSDCardMountLock = null;
+
+// Gonk only
+XPCOMUtils.defineLazyGetter(this, "gExtStorage", function aus_gExtStorage() {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ return Services.env.get("EXTERNAL_STORAGE");
+});
+
+XPCOMUtils.defineLazyModuleGetter(this, "UpdateUtils",
+ "resource://gre/modules/UpdateUtils.jsm");
+
+XPCOMUtils.defineLazyGetter(this, "gLogEnabled", function aus_gLogEnabled() {
+ return getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gUpdateBundle", function aus_gUpdateBundle() {
+ return Services.strings.createBundle(URI_UPDATES_PROPERTIES);
+});
+
+// shared code for suppressing bad cert dialogs
+XPCOMUtils.defineLazyGetter(this, "gCertUtils", function aus_gCertUtils() {
+ let temp = { };
+ Cu.import("resource://gre/modules/CertUtils.jsm", temp);
+ return temp;
+});
+
+/**
+ * Tests to make sure that we can write to a given directory.
+ *
+ * @param updateTestFile a test file in the directory that needs to be tested.
+ * @param createDirectory whether a test directory should be created.
+ * @throws if we don't have right access to the directory.
+ */
+function testWriteAccess(updateTestFile, createDirectory) {
+ const NORMAL_FILE_TYPE = Ci.nsILocalFile.NORMAL_FILE_TYPE;
+ const DIRECTORY_TYPE = Ci.nsILocalFile.DIRECTORY_TYPE;
+ if (updateTestFile.exists())
+ updateTestFile.remove(false);
+ updateTestFile.create(createDirectory ? DIRECTORY_TYPE : NORMAL_FILE_TYPE,
+ createDirectory ? FileUtils.PERMS_DIRECTORY : FileUtils.PERMS_FILE);
+ updateTestFile.remove(false);
+}
+
+/**
+ * Windows only function that closes a Win32 handle.
+ *
+ * @param handle The handle to close
+ */
+function closeHandle(handle) {
+ let lib = ctypes.open("kernel32.dll");
+ let CloseHandle = lib.declare("CloseHandle",
+ ctypes.winapi_abi,
+ ctypes.int32_t, /* success */
+ ctypes.void_t.ptr); /* handle */
+ CloseHandle(handle);
+ lib.close();
+}
+
+/**
+ * Windows only function that creates a mutex.
+ *
+ * @param aName
+ * The name for the mutex.
+ * @param aAllowExisting
+ * If false the function will close the handle and return null.
+ * @return The Win32 handle to the mutex.
+ */
+function createMutex(aName, aAllowExisting = true) {
+ if (AppConstants.platform != "win") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ const INITIAL_OWN = 1;
+ const ERROR_ALREADY_EXISTS = 0xB7;
+ let lib = ctypes.open("kernel32.dll");
+ let CreateMutexW = lib.declare("CreateMutexW",
+ ctypes.winapi_abi,
+ ctypes.void_t.ptr, /* return handle */
+ ctypes.void_t.ptr, /* security attributes */
+ ctypes.int32_t, /* initial owner */
+ ctypes.char16_t.ptr); /* name */
+
+ let handle = CreateMutexW(null, INITIAL_OWN, aName);
+ let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
+ if (handle && !handle.isNull() && !aAllowExisting && alreadyExists) {
+ closeHandle(handle);
+ handle = null;
+ }
+ lib.close();
+
+ if (handle && handle.isNull()) {
+ handle = null;
+ }
+
+ return handle;
+}
+
+/**
+ * Windows only function that determines a unique mutex name for the
+ * installation.
+ *
+ * @param aGlobal true if the function should return a global mutex. A global
+ * mutex is valid across different sessions
+ * @return Global mutex path
+ */
+function getPerInstallationMutexName(aGlobal = true) {
+ if (AppConstants.platform != "win") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA1);
+
+ let exeFile = Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsILocalFile);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ var data = converter.convertToByteArray(exeFile.path.toLowerCase());
+
+ hasher.update(data, data.length);
+ return (aGlobal ? "Global\\" : "") + "MozillaUpdateMutex-" + hasher.finish(true);
+}
+
+/**
+ * Whether or not the current instance has the update mutex. The update mutex
+ * gives protection against 2 applications from the same installation updating:
+ * 1) Running multiple profiles from the same installation path
+ * 2) Two applications running in 2 different user sessions from the same path
+ *
+ * @return true if this instance holds the update mutex
+ */
+function hasUpdateMutex() {
+ if (AppConstants.platform != "win") {
+ return true;
+ }
+ if (!gUpdateMutexHandle) {
+ gUpdateMutexHandle = createMutex(getPerInstallationMutexName(true), false);
+ }
+ return !!gUpdateMutexHandle;
+}
+
+/**
+ * Determines whether or not all descendants of a directory are writeable.
+ * Note: Does not check the root directory itself for writeability.
+ *
+ * @return true if all descendants are writeable, false otherwise
+ */
+function areDirectoryEntriesWriteable(aDir) {
+ let items = aDir.directoryEntries;
+ while (items.hasMoreElements()) {
+ let item = items.getNext().QueryInterface(Ci.nsIFile);
+ if (!item.isWritable()) {
+ LOG("areDirectoryEntriesWriteable - unable to write to " + item.path);
+ return false;
+ }
+ if (item.isDirectory() && !areDirectoryEntriesWriteable(item)) {
+ return false;
+ }
+ }
+ return true;
+}
+
+/**
+ * OSX only function to determine if the user requires elevation to be able to
+ * write to the application bundle.
+ *
+ * @return true if elevation is required, false otherwise
+ */
+function getElevationRequired() {
+ if (AppConstants.platform != "macosx") {
+ return false;
+ }
+
+ try {
+ // Recursively check that the application bundle (and its descendants) can
+ // be written to.
+ LOG("getElevationRequired - recursively testing write access on " +
+ getInstallDirRoot().path);
+ if (!getInstallDirRoot().isWritable() ||
+ !areDirectoryEntriesWriteable(getInstallDirRoot())) {
+ LOG("getElevationRequired - unable to write to application bundle, " +
+ "elevation required");
+ return true;
+ }
+ } catch (ex) {
+ LOG("getElevationRequired - unable to write to application bundle, " +
+ "elevation required. Exception: " + ex);
+ return true;
+ }
+ LOG("getElevationRequired - able to write to application bundle, elevation " +
+ "not required");
+ return false;
+}
+
+/**
+ * Determines whether or not an update can be applied. This is always true on
+ * Windows when the service is used. Also, this is always true on OSX because we
+ * offer users the option to perform an elevated update when necessary.
+ *
+ * @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") {
+ try {
+ let updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
+ LOG("getCanApplyUpdates - testing write access " + updateTestFile.path);
+ testWriteAccess(updateTestFile, false);
+ if (AppConstants.platform == "win") {
+ // Example windowsVersion: Windows XP == 5.1
+ let windowsVersion = Services.sysinfo.getProperty("version");
+ LOG("getCanApplyUpdates - windowsVersion = " + windowsVersion);
+
+ /**
+ * For Vista, updates can be performed to a location requiring admin
+ * privileges by requesting elevation via the UAC prompt when launching
+ * updater.exe if the appDir is under the Program Files directory
+ * (e.g. C:\Program Files\) and UAC is turned on and we can elevate
+ * (e.g. user has a split token).
+ *
+ * Note: this does note attempt to handle the case where UAC is turned on
+ * and the installation directory is in a restricted location that
+ * requires admin privileges to update other than Program Files.
+ */
+ let userCanElevate = false;
+
+ if (parseFloat(windowsVersion) >= 6) {
+ try {
+ // KEY_UPDROOT will fail and throw an exception if
+ // appDir is not under the Program Files, so we rely on that
+ let dir = Services.dirsvc.get(KEY_UPDROOT, Ci.nsIFile);
+ // appDir is under Program Files, so check if the user can elevate
+ userCanElevate = Services.appinfo.QueryInterface(Ci.nsIWinAppHelper).
+ userCanElevate;
+ LOG("getCanApplyUpdates - on Vista, userCanElevate: " + userCanElevate);
+ }
+ catch (ex) {
+ // When the installation directory is not under Program Files,
+ // fall through to checking if write access to the
+ // installation directory is available.
+ LOG("getCanApplyUpdates - on Vista, appDir is not under Program Files");
+ }
+ }
+
+ /**
+ * On Windows, we no longer store the update under the app dir.
+ *
+ * If we are on Windows (including Vista, if we can't elevate) we need to
+ * to check that we can create and remove files from the actual app
+ * directory (like C:\Program Files\Mozilla Firefox). If we can't
+ * (because this user is not an adminstrator, for example) canUpdate()
+ * should return false.
+ *
+ * For Vista, we perform this check to enable updating the application
+ * when the user has write access to the installation directory under the
+ * following scenarios:
+ * 1) the installation directory is not under Program Files
+ * (e.g. C:\Program Files)
+ * 2) UAC is turned off
+ * 3) UAC is turned on and the user is not an admin
+ * (e.g. the user does not have a split token)
+ * 4) UAC is turned on and the user is already elevated, so they can't be
+ * elevated again
+ */
+ if (!userCanElevate) {
+ // if we're unable to create the test file this will throw an exception.
+ let appDirTestFile = getAppBaseDir();
+ appDirTestFile.append(FILE_UPDATE_TEST);
+ LOG("getCanApplyUpdates - testing write access " + appDirTestFile.path);
+ if (appDirTestFile.exists()) {
+ appDirTestFile.remove(false);
+ }
+ appDirTestFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ appDirTestFile.remove(false);
+ }
+ }
+ } catch (e) {
+ LOG("getCanApplyUpdates - unable to apply updates. Exception: " + e);
+ // No write privileges to install directory
+ return false;
+ }
+ } // if (!useService)
+
+ LOG("getCanApplyUpdates - able to apply updates");
+ return true;
+}
+
+/**
+ * Whether or not the application can stage an update for the current session.
+ * These checks are only performed once per session due to using a lazy getter.
+ *
+ * @return true if updates can be staged for this session.
+ */
+XPCOMUtils.defineLazyGetter(this, "gCanStageUpdatesSession", function aus_gCSUS() {
+ if (getElevationRequired()) {
+ LOG("gCanStageUpdatesSession - unable to stage updates because elevation " +
+ "is required.");
+ return false;
+ }
+
+ try {
+ let updateTestFile;
+ if (AppConstants.platform == "macosx") {
+ updateTestFile = getUpdateFile([FILE_UPDATE_TEST]);
+ } else {
+ updateTestFile = getInstallDirRoot();
+ updateTestFile.append(FILE_UPDATE_TEST);
+ }
+ LOG("gCanStageUpdatesSession - testing write access " +
+ updateTestFile.path);
+ testWriteAccess(updateTestFile, true);
+ if (AppConstants.platform != "macosx") {
+ // On all platforms except Mac, we need to test the parent directory as
+ // well, as we need to be able to move files in that directory during the
+ // replacing step.
+ updateTestFile = getInstallDirRoot().parent;
+ updateTestFile.append(FILE_UPDATE_TEST);
+ LOG("gCanStageUpdatesSession - testing write access " +
+ updateTestFile.path);
+ updateTestFile.createUnique(Ci.nsILocalFile.DIRECTORY_TYPE,
+ FileUtils.PERMS_DIRECTORY);
+ updateTestFile.remove(false);
+ }
+ } catch (e) {
+ LOG("gCanStageUpdatesSession - unable to stage updates. Exception: " +
+ e);
+ // No write privileges
+ return false;
+ }
+
+ LOG("gCanStageUpdatesSession - able to stage updates");
+ return true;
+});
+
+/**
+ * Whether or not the application can stage an update.
+ *
+ * @return true if updates can be staged.
+ */
+function getCanStageUpdates() {
+ // If staging updates are disabled, then just bail out!
+ if (!getPref("getBoolPref", PREF_APP_UPDATE_STAGING_ENABLED, false)) {
+ LOG("getCanStageUpdates - staging updates is disabled by preference " +
+ PREF_APP_UPDATE_STAGING_ENABLED);
+ 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;
+ }
+
+ // For Gonk, the updater will remount the /system partition to move staged
+ // files into place.
+ if (AppConstants.platform == "gonk") {
+ LOG("getCanStageUpdates - able to stage updates because this is gonk");
+ return true;
+ }
+
+ if (!hasUpdateMutex()) {
+ LOG("getCanStageUpdates - unable to apply updates because another " +
+ "instance of the application is already handling updates for this " +
+ "installation.");
+ return false;
+ }
+
+ return gCanStageUpdatesSession;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gCanCheckForUpdates", function aus_gCanCheckForUpdates() {
+ // If the administrator has disabled app update and locked the preference so
+ // users can't check for updates. This preference check is ok in this lazy
+ // getter since locked prefs don't change until the application is restarted.
+ var enabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true);
+ if (!enabled && Services.prefs.prefIsLocked(PREF_APP_UPDATE_ENABLED)) {
+ LOG("gCanCheckForUpdates - unable to automatically check for updates, " +
+ "the preference is disabled and admistratively locked.");
+ return false;
+ }
+
+ // If we don't know the binary platform we're updating, we can't update.
+ if (!UpdateUtils.ABI) {
+ LOG("gCanCheckForUpdates - unable to check for updates, unknown ABI");
+ return false;
+ }
+
+ // If we don't know the OS version we're updating, we can't update.
+ if (!UpdateUtils.OSVersion) {
+ LOG("gCanCheckForUpdates - unable to check for updates, unknown OS " +
+ "version");
+ return false;
+ }
+
+ LOG("gCanCheckForUpdates - able to check for updates");
+ return true;
+});
+
+/**
+ * Logs a string to the error console.
+ * @param string
+ * The string to write to the error console.
+ */
+function LOG(string) {
+ if (gLogEnabled) {
+ dump("*** AUS:SVC " + string + "\n");
+ Services.console.logStringMessage("AUS:SVC " + string);
+ }
+}
+
+/**
+ * Gets a preference value, handling the case where there is no default.
+ * @param func
+ * The name of the preference function to call, on nsIPrefBranch
+ * @param preference
+ * The name of the preference
+ * @param defaultValue
+ * The default value to return in the event the preference has
+ * no setting
+ * @return The value of the preference, or undefined if there was no
+ * user or default value.
+ */
+function getPref(func, preference, defaultValue) {
+ try {
+ return Services.prefs[func](preference);
+ }
+ catch (e) {
+ }
+ return defaultValue;
+}
+
+/**
+ * Convert a string containing binary values to hex.
+ */
+function binaryToHex(input) {
+ var result = "";
+ for (var i = 0; i < input.length; ++i) {
+ var hex = input.charCodeAt(i).toString(16);
+ if (hex.length == 1)
+ hex = "0" + hex;
+ result += hex;
+ }
+ return result;
+}
+
+/**
+ * Gets the specified directory at the specified hierarchy under the
+ * update root directory and creates it if it doesn't exist.
+ * @param pathArray
+ * An array of path components to locate beneath the directory
+ * specified by |key|
+ * @return nsIFile object for the location specified.
+ */
+function getUpdateDirCreate(pathArray) {
+ return FileUtils.getDir(KEY_UPDROOT, pathArray, true);
+}
+
+/**
+ * Gets the application base directory.
+ *
+ * @return nsIFile object for the application base directory.
+ */
+function getAppBaseDir() {
+ return Services.dirsvc.get(KEY_EXECUTABLE, Ci.nsIFile).parent;
+}
+
+/**
+ * Gets the root of the installation directory which is the application
+ * bundle directory on Mac OS X and the location of the application binary
+ * on all other platforms.
+ *
+ * @return nsIFile object for the directory
+ */
+function getInstallDirRoot() {
+ let dir = getAppBaseDir();
+ if (AppConstants.platform == "macosx") {
+ // On Mac, we store the Updated.app directory inside the bundle directory.
+ dir = dir.parent.parent;
+ }
+ return dir;
+}
+
+/**
+ * Gets the file at the specified hierarchy under the update root directory.
+ * @param pathArray
+ * An array of path components to locate beneath the directory
+ * specified by |key|. The last item in this array must be the
+ * leaf name of a file.
+ * @return nsIFile object for the file specified. The file is NOT created
+ * if it does not exist, however all required directories along
+ * the way are.
+ */
+function getUpdateFile(pathArray) {
+ let file = getUpdateDirCreate(pathArray.slice(0, -1));
+ file.append(pathArray[pathArray.length - 1]);
+ return file;
+}
+
+/**
+ * Returns human readable status text from the updates.properties bundle
+ * based on an error code
+ * @param code
+ * The error code to look up human readable status text for
+ * @param defaultCode
+ * The default code to look up should human readable status text
+ * not exist for |code|
+ * @return A human readable status text string
+ */
+function getStatusTextFromCode(code, defaultCode) {
+ let reason;
+ try {
+ reason = gUpdateBundle.GetStringFromName("check_error-" + code);
+ LOG("getStatusTextFromCode - transfer error: " + reason + ", code: " +
+ code);
+ }
+ catch (e) {
+ // Use the default reason
+ reason = gUpdateBundle.GetStringFromName("check_error-" + defaultCode);
+ LOG("getStatusTextFromCode - transfer error: " + reason +
+ ", default code: " + defaultCode);
+ }
+ return reason;
+}
+
+/**
+ * Get the Active Updates directory
+ * @return The active updates directory, as a nsIFile object
+ */
+function getUpdatesDir() {
+ // Right now, we only support downloading one patch at a time, so we always
+ // use the same target directory.
+ return getUpdateDirCreate([DIR_UPDATES, "0"]);
+}
+
+/**
+ * Reads the update state from the update.status file in the specified
+ * directory.
+ * @param dir
+ * The dir to look for an update.status file in
+ * @return The status value of the update.
+ */
+function readStatusFile(dir) {
+ let statusFile = dir.clone();
+ statusFile.append(FILE_UPDATE_STATUS);
+ let status = readStringFromFile(statusFile) || STATE_NONE;
+ LOG("readStatusFile - status: " + status + ", path: " + statusFile.path);
+ return status;
+}
+
+/**
+ * Writes the current update operation/state to a file in the patch
+ * directory, indicating to the patching system that operations need
+ * to be performed.
+ * @param dir
+ * The patch directory where the update.status file should be
+ * written.
+ * @param state
+ * The state value to write.
+ */
+function writeStatusFile(dir, state) {
+ let statusFile = dir.clone();
+ statusFile.append(FILE_UPDATE_STATUS);
+ writeStringToFile(statusFile, state);
+}
+
+/**
+ * Writes the update's application version to a file in the patch directory. If
+ * the update doesn't provide application version information via the
+ * appVersion attribute the string "null" will be written to the file.
+ * This value is compared during startup (in nsUpdateDriver.cpp) to determine if
+ * the update should be applied. Note that this won't provide protection from
+ * downgrade of the application for the nightly user case where the application
+ * version doesn't change.
+ * @param dir
+ * The patch directory where the update.version file should be
+ * written.
+ * @param version
+ * The version value to write. Will be the string "null" when the
+ * update doesn't provide the appVersion attribute in the update xml.
+ */
+function writeVersionFile(dir, version) {
+ let versionFile = dir.clone();
+ versionFile.append(FILE_UPDATE_VERSION);
+ writeStringToFile(versionFile, version);
+}
+
+/**
+ * Gonk only function that reads the link file specified in the update.link file
+ * in the specified directory and returns the nsIFile for the corresponding
+ * file.
+ * @param dir
+ * The dir to look for an update.link file in
+ * @return A nsIFile for the file path specified in the
+ * update.link file or null if the update.link file
+ * doesn't exist.
+ */
+function getFileFromUpdateLink(dir) {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ let linkFile = dir.clone();
+ linkFile.append(FILE_UPDATE_LINK);
+ let link = readStringFromFile(linkFile);
+ LOG("getFileFromUpdateLink linkFile.path: " + linkFile.path + ", link: " + link);
+ if (!link) {
+ return null;
+ }
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(link);
+ return file;
+}
+
+/**
+ * Gonk only function to create a link file. This allows the actual patch to
+ * live in a directory different from the update directory.
+ * @param dir
+ * The patch directory where the update.link file
+ * should be written.
+ * @param patchFile
+ * The fully qualified filename of the patchfile.
+ */
+function writeLinkFile(dir, patchFile) {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ let linkFile = dir.clone();
+ linkFile.append(FILE_UPDATE_LINK);
+ writeStringToFile(linkFile, patchFile.path);
+ if (patchFile.path.indexOf(gExtStorage) == 0) {
+ // The patchfile is being stored on external storage. Try to lock it
+ // so that it doesn't get shared with the PC while we're downloading
+ // to it.
+ acquireSDCardMountLock();
+ }
+}
+
+/**
+ * Gonk only function to acquire a VolumeMountLock for the sdcard volume.
+ *
+ * This prevents the SDCard from being shared with the PC while
+ * we're downloading the update.
+ */
+function acquireSDCardMountLock() {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ let volsvc = Cc["@mozilla.org/telephony/volume-service;1"].
+ getService(Ci.nsIVolumeService);
+ if (volsvc) {
+ gSDCardMountLock = volsvc.createMountLock("sdcard");
+ }
+}
+
+/**
+ * Gonk only function that determines if the state corresponds to an
+ * interrupted update. This could either be because the download was
+ * interrupted, or because staging the update was interrupted.
+ *
+ * @return true if the state corresponds to an interrupted
+ * update.
+ */
+function isInterruptedUpdate(status) {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ }
+ return (status == STATE_DOWNLOADING) ||
+ (status == STATE_PENDING) ||
+ (status == STATE_APPLYING);
+}
+
+/**
+ * Releases any SDCard mount lock that we might have.
+ *
+ * This once again allows the SDCard to be shared with the PC.
+ */
+function releaseSDCardMountLock() {
+ if (AppConstants.platform != "gonk") {
+ throw Cr.NS_ERROR_UNEXPECTED;
+ }
+ if (gSDCardMountLock) {
+ gSDCardMountLock.unlock();
+ gSDCardMountLock = null;
+ }
+}
+
+/**
+ * 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
+ * directory of the patch directory and then move the update.log in the patch
+ * directory to last-update.log in the parent directory of the patch directory.
+ *
+ * @param aRemovePatchFiles (optional, defaults to true)
+ * When true the update's patch directory contents are removed.
+ */
+function cleanUpUpdatesDir(aRemovePatchFiles = true) {
+ let updateDir;
+ try {
+ updateDir = getUpdatesDir();
+ } catch (e) {
+ LOG("cleanUpUpdatesDir - unable to get the updates patch directory. " +
+ "Exception: " + e);
+ return;
+ }
+
+ // Preserve the last update log file for debugging purposes.
+ let updateLogFile = updateDir.clone();
+ updateLogFile.append(FILE_UPDATE_LOG);
+ if (updateLogFile.exists()) {
+ let dir = updateDir.parent;
+ let logFile = dir.clone();
+ logFile.append(FILE_LAST_UPDATE_LOG);
+ if (logFile.exists()) {
+ try {
+ logFile.moveTo(dir, FILE_BACKUP_UPDATE_LOG);
+ } catch (e) {
+ LOG("cleanUpUpdatesDir - failed to rename file " + logFile.path +
+ " to " + FILE_BACKUP_UPDATE_LOG);
+ }
+ }
+
+ try {
+ updateLogFile.moveTo(dir, FILE_LAST_UPDATE_LOG);
+ } catch (e) {
+ LOG("cleanUpUpdatesDir - failed to rename file " + updateLogFile.path +
+ " to " + FILE_LAST_UPDATE_LOG);
+ }
+ }
+
+ if (aRemovePatchFiles) {
+ let dirEntries = updateDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let file = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+ if (AppConstants.platform == "gonk") {
+ if (file.leafName == FILE_UPDATE_LINK) {
+ let linkedFile = getFileFromUpdateLink(updateDir);
+ if (linkedFile && linkedFile.exists()) {
+ linkedFile.remove(false);
+ }
+ }
+ }
+
+ // Now, recursively remove this file. The recursive removal is needed for
+ // Mac OSX because this directory will contain a copy of updater.app,
+ // which is itself a directory and the MozUpdater directory on platforms
+ // other than Windows.
+ try {
+ file.remove(true);
+ } catch (e) {
+ LOG("cleanUpUpdatesDir - failed to remove file " + file.path);
+ }
+ }
+ }
+ if (AppConstants.platform == "gonk") {
+ releaseSDCardMountLock();
+ }
+}
+
+/**
+ * Clean up updates list and the updates directory.
+ */
+function cleanupActiveUpdate() {
+ // Move the update from the Active Update list into the Past Updates list.
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ um.activeUpdate = null;
+ um.saveUpdates();
+
+ // Now trash the updates directory, since we're done with it
+ cleanUpUpdatesDir();
+}
+
+/**
+ * An enumeration of items in a JS array.
+ * @constructor
+ */
+function ArrayEnumerator(aItems) {
+ this._index = 0;
+ if (aItems) {
+ for (var i = 0; i < aItems.length; ++i) {
+ if (!aItems[i])
+ aItems.splice(i, 1);
+ }
+ }
+ this._contents = aItems;
+}
+
+ArrayEnumerator.prototype = {
+ _index: 0,
+ _contents: [],
+
+ hasMoreElements: function ArrayEnumerator_hasMoreElements() {
+ return this._index < this._contents.length;
+ },
+
+ getNext: function ArrayEnumerator_getNext() {
+ return this._contents[this._index++];
+ }
+};
+
+/**
+ * Writes a string of text to a file. A newline will be appended to the data
+ * written to the file. This function only works with ASCII text.
+ */
+function writeStringToFile(file, text) {
+ let fos = FileUtils.openSafeFileOutputStream(file);
+ text += "\n";
+ fos.write(text, text.length);
+ FileUtils.closeSafeFileOutputStream(fos);
+}
+
+function readStringFromInputStream(inputStream) {
+ var sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(inputStream);
+ var text = sis.read(sis.available());
+ sis.close();
+ if (text && text[text.length - 1] == "\n") {
+ text = text.slice(0, -1);
+ }
+ return text;
+}
+
+/**
+ * Reads a string of text from a file. A trailing newline will be removed
+ * before the result is returned. This function only works with ASCII text.
+ */
+function readStringFromFile(file) {
+ if (!file.exists()) {
+ LOG("readStringFromFile - file doesn't exist: " + file.path);
+ return null;
+ }
+ var fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ return readStringFromInputStream(fis);
+}
+
+function handleUpdateFailure(update, errorCode) {
+ update.errorCode = parseInt(errorCode);
+ if (update.errorCode == FOTA_GENERAL_ERROR ||
+ update.errorCode == FOTA_FILE_OPERATION_ERROR ||
+ update.errorCode == FOTA_RECOVERY_ERROR ||
+ update.errorCode == FOTA_UNKNOWN_ERROR) {
+ // In the case of FOTA update errors, don't reset the state to pending. This
+ // causes the FOTA update path to try again, which is not necessarily what
+ // we want.
+ update.statusText = gUpdateBundle.GetStringFromName("statusFailed");
+
+ Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt).
+ showUpdateError(update);
+ writeStatusFile(getUpdatesDir(), STATE_FAILED + ": " + errorCode);
+ cleanupActiveUpdate();
+ return true;
+ }
+
+ // Replace with Array.prototype.includes when it has stabilized.
+ if (WRITE_ERRORS.indexOf(update.errorCode) != -1 ||
+ update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR) {
+ Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt).
+ showUpdateError(update);
+ writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
+ return true;
+ }
+
+ if (update.errorCode == ELEVATION_CANCELED) {
+ let cancelations = getPref("getIntPref", PREF_APP_UPDATE_CANCELATIONS, 0);
+ cancelations++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS, cancelations);
+ if (AppConstants.platform == "macosx") {
+ let osxCancelations = getPref("getIntPref",
+ PREF_APP_UPDATE_CANCELATIONS_OSX, 0);
+ osxCancelations++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_CANCELATIONS_OSX,
+ osxCancelations);
+ let maxCancels = getPref("getIntPref",
+ PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
+ DEFAULT_CANCELATIONS_OSX_MAX);
+ // Prevent the preference from setting a value greater than 5.
+ maxCancels = Math.min(maxCancels, 5);
+ if (osxCancelations >= maxCancels) {
+ cleanupActiveUpdate();
+ } else {
+ writeStatusFile(getUpdatesDir(),
+ update.state = STATE_PENDING_ELEVATE);
+ }
+ update.statusText = gUpdateBundle.GetStringFromName("elevationFailure");
+ update.QueryInterface(Ci.nsIWritablePropertyBag);
+ update.setProperty("patchingFailed", "elevationFailure");
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(update);
+ } else {
+ writeStatusFile(getUpdatesDir(), update.state = STATE_PENDING);
+ }
+ return true;
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
+ 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;
+}
+
+/**
+ * Fall back to downloading a complete update in case an update has failed.
+ *
+ * @param update the update object that has failed to apply.
+ * @param postStaging true if we have just attempted to stage an update.
+ */
+function handleFallbackToCompleteUpdate(update, postStaging) {
+ cleanupActiveUpdate();
+
+ update.statusText = gUpdateBundle.GetStringFromName("patchApplyFailure");
+ var oldType = update.selectedPatch ? update.selectedPatch.type
+ : "complete";
+ if (update.selectedPatch && oldType == "partial" && update.patchCount == 2) {
+ // Partial patch application failed, try downloading the complete
+ // update in the background instead.
+ LOG("handleFallbackToCompleteUpdate - install of partial patch " +
+ "failed, downloading complete patch");
+ var status = Cc["@mozilla.org/updates/update-service;1"].
+ getService(Ci.nsIApplicationUpdateService).
+ downloadUpdate(update, !postStaging);
+ if (status == STATE_NONE)
+ cleanupActiveUpdate();
+ }
+ else {
+ LOG("handleFallbackToCompleteUpdate - install of complete or " +
+ "only one patch offered failed.");
+ }
+ update.QueryInterface(Ci.nsIWritablePropertyBag);
+ update.setProperty("patchingFailed", oldType);
+}
+
+function pingStateAndStatusCodes(aUpdate, aStartup, aStatus) {
+ let patchType = AUSTLMY.PATCH_UNKNOWN;
+ if (aUpdate && aUpdate.selectedPatch && aUpdate.selectedPatch.type) {
+ if (aUpdate.selectedPatch.type == "complete") {
+ patchType = AUSTLMY.PATCH_COMPLETE;
+ } else if (aUpdate.selectedPatch.type == "partial") {
+ patchType = AUSTLMY.PATCH_PARTIAL;
+ }
+ }
+
+ let suffix = patchType + "_" + (aStartup ? AUSTLMY.STARTUP : AUSTLMY.STAGE);
+ let stateCode = 0;
+ let parts = aStatus.split(":");
+ if (parts.length > 0) {
+ switch (parts[0]) {
+ case STATE_NONE:
+ stateCode = 2;
+ break;
+ case STATE_DOWNLOADING:
+ stateCode = 3;
+ break;
+ case STATE_PENDING:
+ stateCode = 4;
+ break;
+ case STATE_PENDING_SERVICE:
+ stateCode = 5;
+ break;
+ case STATE_APPLYING:
+ stateCode = 6;
+ break;
+ case STATE_APPLIED:
+ stateCode = 7;
+ break;
+ case STATE_APPLIED_OS:
+ stateCode = 8;
+ break;
+ case STATE_APPLIED_SERVICE:
+ stateCode = 9;
+ break;
+ case STATE_SUCCEEDED:
+ stateCode = 10;
+ break;
+ case STATE_DOWNLOAD_FAILED:
+ stateCode = 11;
+ break;
+ case STATE_FAILED:
+ stateCode = 12;
+ break;
+ case STATE_PENDING_ELEVATE:
+ stateCode = 13;
+ break;
+ default:
+ stateCode = 1;
+ }
+
+ if (parts.length > 1) {
+ let statusErrorCode = INVALID_UPDATER_STATE_CODE;
+ if (parts[0] == STATE_FAILED) {
+ statusErrorCode = parseInt(parts[1]) || INVALID_UPDATER_STATUS_CODE;
+ }
+ AUSTLMY.pingStatusErrorCode(suffix, statusErrorCode);
+ }
+ }
+ AUSTLMY.pingStateCode(suffix, stateCode);
+}
+
+/**
+ * Update Patch
+ * @param patch
+ * A <patch> element to initialize this object with
+ * @throws if patch has a size of 0
+ * @constructor
+ */
+function UpdatePatch(patch) {
+ this._properties = {};
+ for (var i = 0; i < patch.attributes.length; ++i) {
+ var attr = patch.attributes.item(i);
+ attr.QueryInterface(Ci.nsIDOMAttr);
+ switch (attr.name) {
+ case "selected":
+ this.selected = attr.value == "true";
+ break;
+ case "size":
+ if (0 == parseInt(attr.value)) {
+ LOG("UpdatePatch:init - 0-sized patch!");
+ throw Cr.NS_ERROR_ILLEGAL_VALUE;
+ }
+ // fall through
+ default:
+ this[attr.name] = attr.value;
+ break;
+ }
+ }
+}
+UpdatePatch.prototype = {
+ /**
+ * See nsIUpdateService.idl
+ */
+ serialize: function UpdatePatch_serialize(updates) {
+ var patch = updates.createElementNS(URI_UPDATE_NS, "patch");
+ patch.setAttribute("type", this.type);
+ patch.setAttribute("URL", this.URL);
+ // finalURL is not available until after the download has started
+ if (this.finalURL) {
+ patch.setAttribute("finalURL", this.finalURL);
+ }
+ patch.setAttribute("hashFunction", this.hashFunction);
+ patch.setAttribute("hashValue", this.hashValue);
+ patch.setAttribute("size", this.size);
+ if (this.selected) {
+ patch.setAttribute("selected", this.selected);
+ }
+ patch.setAttribute("state", this.state);
+
+ for (let p in this._properties) {
+ if (this._properties[p].present) {
+ patch.setAttribute(p, this._properties[p].data);
+ }
+ }
+
+ return patch;
+ },
+
+ /**
+ * A hash of custom properties
+ */
+ _properties: null,
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ setProperty: function UpdatePatch_setProperty(name, value) {
+ this._properties[name] = { data: value, present: true };
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ deleteProperty: function UpdatePatch_deleteProperty(name) {
+ if (name in this._properties)
+ this._properties[name].present = false;
+ else
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ */
+ get enumerator() {
+ var properties = [];
+ for (var p in this._properties)
+ properties.push(this._properties[p].data);
+ return new ArrayEnumerator(properties);
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ * Note: returns null instead of throwing when the property doesn't exist to
+ * simplify code and to silence warnings in debug builds.
+ */
+ getProperty: function UpdatePatch_getProperty(name) {
+ if (name in this._properties &&
+ this._properties[name].present) {
+ return this._properties[name].data;
+ }
+ return null;
+ },
+
+ /**
+ * Returns whether or not the update.status file for this patch exists at the
+ * appropriate location.
+ */
+ get statusFileExists() {
+ var statusFile = getUpdatesDir();
+ statusFile.append(FILE_UPDATE_STATUS);
+ return statusFile.exists();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get state() {
+ if (this._properties.state)
+ return this._properties.state;
+ return STATE_NONE;
+ },
+ set state(val) {
+ this._properties.state = val;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePatch,
+ Ci.nsIPropertyBag,
+ Ci.nsIWritablePropertyBag])
+};
+
+/**
+ * Update
+ * Implements nsIUpdate
+ * @param update
+ * An <update> element to initialize this object with
+ * @throws if the update contains no patches
+ * @constructor
+ */
+function Update(update) {
+ this._properties = {};
+ this._patches = [];
+ this.isCompleteUpdate = false;
+ this.isOSUpdate = false;
+ this.showPrompt = false;
+ this.showNeverForVersion = false;
+ this.unsupported = false;
+ this.channel = "default";
+ this.promptWaitTime = getPref("getIntPref", PREF_APP_UPDATE_PROMPTWAITTIME, 43200);
+ this.backgroundInterval = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDINTERVAL,
+ DOWNLOAD_BACKGROUND_INTERVAL);
+
+ // Null <update>, assume this is a message container and do no
+ // further initialization
+ if (!update) {
+ return;
+ }
+
+ const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+ let patch;
+ for (let i = 0; i < update.childNodes.length; ++i) {
+ let patchElement = update.childNodes.item(i);
+ if (patchElement.nodeType != ELEMENT_NODE ||
+ patchElement.localName != "patch") {
+ continue;
+ }
+
+ patchElement.QueryInterface(Ci.nsIDOMElement);
+ try {
+ patch = new UpdatePatch(patchElement);
+ } catch (e) {
+ continue;
+ }
+ this._patches.push(patch);
+ }
+
+ if (this._patches.length == 0 && !update.hasAttribute("unsupported")) {
+ throw Cr.NS_ERROR_ILLEGAL_VALUE;
+ }
+
+ // Set the installDate value with the current time. If the update has an
+ // installDate attribute this will be replaced with that value if it doesn't
+ // equal 0.
+ this.installDate = (new Date()).getTime();
+
+ for (let i = 0; i < update.attributes.length; ++i) {
+ var attr = update.attributes.item(i);
+ attr.QueryInterface(Ci.nsIDOMAttr);
+ if (attr.value == "undefined") {
+ continue;
+ } else if (attr.name == "detailsURL") {
+ this._detailsURL = attr.value;
+ } else if (attr.name == "installDate" && attr.value) {
+ let val = parseInt(attr.value);
+ if (val) {
+ this.installDate = val;
+ }
+ } else if (attr.name == "isCompleteUpdate") {
+ this.isCompleteUpdate = attr.value == "true";
+ } else if (attr.name == "isSecurityUpdate") {
+ this.isSecurityUpdate = attr.value == "true";
+ } else if (attr.name == "isOSUpdate") {
+ this.isOSUpdate = attr.value == "true";
+ } else if (attr.name == "showNeverForVersion") {
+ this.showNeverForVersion = attr.value == "true";
+ } else if (attr.name == "showPrompt") {
+ this.showPrompt = attr.value == "true";
+ } else if (attr.name == "promptWaitTime") {
+ if (!isNaN(attr.value)) {
+ this.promptWaitTime = parseInt(attr.value);
+ }
+ } else if (attr.name == "backgroundInterval") {
+ if (!isNaN(attr.value)) {
+ this.backgroundInterval = parseInt(attr.value);
+ }
+ } else if (attr.name == "unsupported") {
+ this.unsupported = attr.value == "true";
+ } else {
+ this[attr.name] = attr.value;
+
+ switch (attr.name) {
+ case "appVersion":
+ case "buildID":
+ case "channel":
+ case "displayVersion":
+ case "name":
+ case "previousAppVersion":
+ case "serviceURL":
+ case "statusText":
+ case "type":
+ break;
+ default:
+ // Save custom attributes when serializing to the local xml file but
+ // don't use this method for the expected attributes which are already
+ // handled in serialize.
+ this.setProperty(attr.name, attr.value);
+ break;
+ }
+ }
+ }
+
+ if (!this.displayVersion) {
+ this.displayVersion = this.appVersion;
+ }
+
+ // Don't allow the background download interval to be greater than 10 minutes.
+ this.backgroundInterval = Math.min(this.backgroundInterval, 600);
+
+ // The Update Name is either the string provided by the <update> element, or
+ // the string: "<App Name> <Update App Version>"
+ var name = "";
+ if (update.hasAttribute("name")) {
+ name = update.getAttribute("name");
+ } else {
+ var brandBundle = Services.strings.createBundle(URI_BRAND_PROPERTIES);
+ var appName = brandBundle.GetStringFromName("brandShortName");
+ name = gUpdateBundle.formatStringFromName("updateName",
+ [appName, this.displayVersion], 2);
+ }
+ this.name = name;
+}
+Update.prototype = {
+ /**
+ * See nsIUpdateService.idl
+ */
+ get patchCount() {
+ return this._patches.length;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getPatchAt: function Update_getPatchAt(index) {
+ return this._patches[index];
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ *
+ * We use a copy of the state cached on this object in |_state| only when
+ * there is no selected patch, i.e. in the case when we could not load
+ * |.activeUpdate| from the update manager for some reason but still have
+ * the update.status file to work with.
+ */
+ _state: "",
+ set state(state) {
+ if (this.selectedPatch)
+ this.selectedPatch.state = state;
+ this._state = state;
+ return state;
+ },
+ get state() {
+ if (this.selectedPatch)
+ return this.selectedPatch.state;
+ return this._state;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ errorCode: 0,
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get selectedPatch() {
+ for (var i = 0; i < this.patchCount; ++i) {
+ if (this._patches[i].selected)
+ return this._patches[i];
+ }
+ return null;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get detailsURL() {
+ if (!this._detailsURL) {
+ try {
+ // Try using a default details URL supplied by the distribution
+ // if the update XML does not supply one.
+ return Services.urlFormatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS);
+ }
+ catch (e) {
+ }
+ }
+ return this._detailsURL || "";
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ serialize: function Update_serialize(updates) {
+ // If appVersion isn't defined just return null. This happens when cleaning
+ // up invalid updates (e.g. incorrect channel).
+ if (!this.appVersion) {
+ return null;
+ }
+ var update = updates.createElementNS(URI_UPDATE_NS, "update");
+ update.setAttribute("appVersion", this.appVersion);
+ update.setAttribute("buildID", this.buildID);
+ update.setAttribute("channel", this.channel);
+ update.setAttribute("displayVersion", this.displayVersion);
+ update.setAttribute("installDate", this.installDate);
+ update.setAttribute("isCompleteUpdate", this.isCompleteUpdate);
+ update.setAttribute("isOSUpdate", this.isOSUpdate);
+ update.setAttribute("name", this.name);
+ update.setAttribute("serviceURL", this.serviceURL);
+ update.setAttribute("showNeverForVersion", this.showNeverForVersion);
+ update.setAttribute("showPrompt", this.showPrompt);
+ update.setAttribute("promptWaitTime", this.promptWaitTime);
+ update.setAttribute("backgroundInterval", this.backgroundInterval);
+ update.setAttribute("type", this.type);
+
+ if (this.detailsURL) {
+ update.setAttribute("detailsURL", this.detailsURL);
+ }
+ if (this.previousAppVersion) {
+ update.setAttribute("previousAppVersion", this.previousAppVersion);
+ }
+ if (this.statusText) {
+ update.setAttribute("statusText", this.statusText);
+ }
+ if (this.unsupported) {
+ update.setAttribute("unsupported", this.unsupported);
+ }
+ updates.documentElement.appendChild(update);
+
+ for (let p in this._properties) {
+ if (this._properties[p].present) {
+ update.setAttribute(p, this._properties[p].data);
+ }
+ }
+
+ for (let i = 0; i < this.patchCount; ++i) {
+ update.appendChild(this.getPatchAt(i).serialize(updates));
+ }
+
+ return update;
+ },
+
+ /**
+ * A hash of custom properties
+ */
+ _properties: null,
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ setProperty: function Update_setProperty(name, value) {
+ this._properties[name] = { data: value, present: true };
+ },
+
+ /**
+ * See nsIWritablePropertyBag.idl
+ */
+ deleteProperty: function Update_deleteProperty(name) {
+ if (name in this._properties)
+ this._properties[name].present = false;
+ else
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ */
+ get enumerator() {
+ var properties = [];
+ for (let p in this._properties) {
+ properties.push(this._properties[p].data);
+ }
+ return new ArrayEnumerator(properties);
+ },
+
+ /**
+ * See nsIPropertyBag.idl
+ * Note: returns null instead of throwing when the property doesn't exist to
+ * simplify code and to silence warnings in debug builds.
+ */
+ getProperty: function Update_getProperty(name) {
+ if (name in this._properties && this._properties[name].present) {
+ return this._properties[name].data;
+ }
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdate,
+ Ci.nsIPropertyBag,
+ Ci.nsIWritablePropertyBag])
+};
+
+const UpdateServiceFactory = {
+ _instance: null,
+ createInstance: function (outer, iid) {
+ if (outer != null)
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ return this._instance == null ? this._instance = new UpdateService() :
+ this._instance;
+ }
+};
+
+/**
+ * UpdateService
+ * A Service for managing the discovery and installation of software updates.
+ * @constructor
+ */
+function UpdateService() {
+ LOG("Creating UpdateService");
+ Services.obs.addObserver(this, "xpcom-shutdown", false);
+ Services.prefs.addObserver(PREF_APP_UPDATE_LOG, this, false);
+ if (AppConstants.platform == "gonk") {
+ // PowerManagerService::SyncProfile (which is called for Reboot, PowerOff
+ // and Restart) sends the profile-change-net-teardown event. We can then
+ // pause the download in a similar manner to xpcom-shutdown.
+ Services.obs.addObserver(this, "profile-change-net-teardown", false);
+ }
+}
+
+UpdateService.prototype = {
+ /**
+ * The downloader we are using to download updates. There is only ever one of
+ * these.
+ */
+ _downloader: null,
+
+ /**
+ * Whether or not the service registered the "online" observer.
+ */
+ _registeredOnlineObserver: false,
+
+ /**
+ * The current number of consecutive socket errors
+ */
+ _consecutiveSocketErrors: 0,
+
+ /**
+ * A timer used to retry socket errors
+ */
+ _retryTimer: null,
+
+ /**
+ * Whether or not a background update check was initiated by the
+ * application update timer notification.
+ */
+ _isNotify: true,
+
+ /**
+ * Handle Observer Service notifications
+ * @param subject
+ * The subject of the notification
+ * @param topic
+ * The notification name
+ * @param data
+ * Additional data
+ */
+ observe: function AUS_observe(subject, topic, data) {
+ switch (topic) {
+ case "post-update-processing":
+ if (readStatusFile(getUpdatesDir()) == STATE_SUCCEEDED) {
+ // The active update needs to be copied to the first update in the
+ // updates.xml early during startup to support post update actions
+ // (bug 1301288).
+ let um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ um.activeUpdate.state = STATE_SUCCEEDED;
+ um.saveUpdates();
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_POSTUPDATE, true);
+ }
+
+ if (Services.appinfo.ID in APPID_TO_TOPIC) {
+ // Delay post-update processing to ensure that possible update
+ // dialogs are shown in front of the app window, if possible.
+ // See bug 311614.
+ Services.obs.addObserver(this, APPID_TO_TOPIC[Services.appinfo.ID],
+ false);
+ break;
+ }
+ // intentional fallthrough
+ case "sessionstore-windows-restored":
+ case "mail-startup-done":
+ case "xul-window-visible":
+ if (Services.appinfo.ID in APPID_TO_TOPIC) {
+ Services.obs.removeObserver(this,
+ APPID_TO_TOPIC[Services.appinfo.ID]);
+ }
+ // intentional fallthrough
+ case "test-post-update-processing":
+ // Clean up any extant updates
+ this._postUpdateProcessing();
+ break;
+ case "network:offline-status-changed":
+ this._offlineStatusChanged(data);
+ break;
+ case "nsPref:changed":
+ if (data == PREF_APP_UPDATE_LOG) {
+ gLogEnabled = getPref("getBoolPref", PREF_APP_UPDATE_LOG, false);
+ }
+ break;
+ case "profile-change-net-teardown": // fall thru
+ case "xpcom-shutdown":
+ Services.obs.removeObserver(this, topic);
+ Services.prefs.removeObserver(PREF_APP_UPDATE_LOG, this);
+
+ if (AppConstants.platform == "win" && gUpdateMutexHandle) {
+ // If we hold the update mutex, let it go!
+ // The OS would clean this up sometime after shutdown,
+ // but that would have no guarantee on timing.
+ closeHandle(gUpdateMutexHandle);
+ }
+ if (this._retryTimer) {
+ this._retryTimer.cancel();
+ }
+
+ this.pauseDownload();
+ // Prevent leaking the downloader (bug 454964)
+ this._downloader = null;
+ break;
+ }
+ },
+
+ /**
+ * The following needs to happen during the post-update-processing
+ * notification from nsUpdateServiceStub.js:
+ * 1. post update processing
+ * 2. resume of a download that was in progress during a previous session
+ * 3. start of a complete update download after the failure to apply a partial
+ * update
+ */
+
+ /**
+ * Perform post-processing on updates lingering in the updates directory
+ * from a previous application session - either report install failures (and
+ * optionally attempt to fetch a different version if appropriate) or
+ * notify the user of install success.
+ */
+ _postUpdateProcessing: function AUS__postUpdateProcessing() {
+ if (!this.canCheckForUpdates) {
+ LOG("UpdateService:_postUpdateProcessing - unable to check for " +
+ "updates... returning early");
+ return;
+ }
+
+ if (!this.canApplyUpdates) {
+ LOG("UpdateService:_postUpdateProcessing - unable to apply " +
+ "updates... returning early");
+ // If the update is present in the update directly somehow,
+ // it would prevent us from notifying the user of futher updates.
+ cleanupActiveUpdate();
+ return;
+ }
+
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ var update = um.activeUpdate;
+ var status = readStatusFile(getUpdatesDir());
+ pingStateAndStatusCodes(update, true, status);
+ // STATE_NONE status typically means that the update.status file is present
+ // but a background download error occurred.
+ if (status == STATE_NONE) {
+ LOG("UpdateService:_postUpdateProcessing - no status, no update");
+ cleanupActiveUpdate();
+ return;
+ }
+
+ // Handle the case when the update is the same or older than the current
+ // 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_ELEVATE)) {
+ if (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 ||
+ Services.vc.compare(update.appVersion, Services.appinfo.version) == 0 &&
+ update.buildID == Services.appinfo.appBuildID) {
+ LOG("UpdateService:_postUpdateProcessing - removing update for older " +
+ "or same application version");
+ cleanupActiveUpdate();
+ return;
+ }
+ }
+
+ if (AppConstants.platform == "gonk") {
+ // This code is called very early in the boot process, before we've even
+ // had a chance to setup the UI so we can give feedback to the user.
+ //
+ // Since the download may be occuring over a link which has associated
+ // cost, we want to require user-consent before resuming the download.
+ // Also, applying an already downloaded update now is undesireable,
+ // since the phone will look dead while the update is being applied.
+ // Applying the update can take several minutes. Instead we wait until
+ // the UI is initialized so it is possible to give feedback to and get
+ // consent to update from the user.
+ if (isInterruptedUpdate(status)) {
+ LOG("UpdateService:_postUpdateProcessing - interrupted update detected - wait for user consent");
+ return;
+ }
+ }
+
+ if (status == STATE_DOWNLOADING) {
+ LOG("UpdateService:_postUpdateProcessing - patch found in downloading " +
+ "state");
+ if (update && update.state != STATE_SUCCEEDED) {
+ // Resume download
+ status = this.downloadUpdate(update, true);
+ if (status == STATE_NONE)
+ cleanupActiveUpdate();
+ }
+ return;
+ }
+
+ if (status == STATE_APPLYING) {
+ // This indicates that the background updater service is in either of the
+ // following two states:
+ // 1. It is in the process of applying an update in the background, and
+ // we just happen to be racing against that.
+ // 2. It has failed to apply an update for some reason, and we hit this
+ // case because the updater process has set the update status to
+ // applying, but has never finished.
+ // In order to differentiate between these two states, we look at the
+ // state field of the update object. If it's "pending", then we know
+ // that this is the first time we're hitting this case, so we switch
+ // 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)) {
+ LOG("UpdateService:_postUpdateProcessing - patch found in applying " +
+ "state for the first time");
+ update.state = STATE_APPLYING;
+ um.saveUpdates();
+ } else { // We get here even if we don't have an update object
+ LOG("UpdateService:_postUpdateProcessing - patch found in applying " +
+ "state for the second time");
+ cleanupActiveUpdate();
+ }
+ return;
+ }
+
+ if (AppConstants.platform == "gonk") {
+ // The update is only applied but not selected to be installed
+ if (status == STATE_APPLIED && update && update.isOSUpdate) {
+ LOG("UpdateService:_postUpdateProcessing - update staged as applied found");
+ return;
+ }
+
+ if (status == STATE_APPLIED_OS && update && update.isOSUpdate) {
+ // In gonk, we need to check for OS update status after startup, since
+ // the recovery partition won't write to update.status for us
+ let recoveryService = Cc["@mozilla.org/recovery-service;1"].
+ getService(Ci.nsIRecoveryService);
+ let fotaStatus = recoveryService.getFotaUpdateStatus();
+ switch (fotaStatus) {
+ case Ci.nsIRecoveryService.FOTA_UPDATE_SUCCESS:
+ status = STATE_SUCCEEDED;
+ break;
+ case Ci.nsIRecoveryService.FOTA_UPDATE_FAIL:
+ status = STATE_FAILED + ": " + FOTA_GENERAL_ERROR;
+ break;
+ case Ci.nsIRecoveryService.FOTA_UPDATE_UNKNOWN:
+ default:
+ status = STATE_FAILED + ": " + FOTA_UNKNOWN_ERROR;
+ break;
+ }
+ }
+ }
+
+ if (!update) {
+ if (status != STATE_SUCCEEDED) {
+ LOG("UpdateService:_postUpdateProcessing - previous patch failed " +
+ "and no patch available");
+ cleanupActiveUpdate();
+ return;
+ }
+ update = new Update(null);
+ }
+
+ let parts = status.split(":");
+ update.state = parts[0];
+ if (update.state == STATE_FAILED && parts[1]) {
+ update.errorCode = parseInt(parts[1]);
+ }
+
+
+ if (status != STATE_SUCCEEDED) {
+ // Since the update didn't succeed save a copy of the active update's
+ // current state to the updates.xml so it is possible to track failures.
+ um.saveUpdates();
+ // Rotate the update logs so the update log isn't removed. By passing
+ // false the patch directory won't be removed.
+ cleanUpUpdatesDir(false);
+ }
+
+ if (status == STATE_SUCCEEDED) {
+ update.statusText = gUpdateBundle.GetStringFromName("installSuccess");
+
+ // Update the patch's metadata.
+ um.activeUpdate = update;
+
+ // Done with this update. Clean it up.
+ cleanupActiveUpdate();
+ } else if (status == STATE_PENDING_ELEVATE) {
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateElevationRequired();
+ return;
+ } else {
+ // If there was an I/O error it is assumed that the patch is not invalid
+ // and it is set to pending so an attempt to apply it again will happen
+ // when the application is restarted.
+ if (update.state == STATE_FAILED && update.errorCode) {
+ if (handleUpdateFailure(update, update.errorCode)) {
+ return;
+ }
+ }
+
+ // Something went wrong with the patch application process.
+ handleFallbackToCompleteUpdate(update, false);
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(update);
+ }
+ },
+
+ /**
+ * Register an observer when the network comes online, so we can short-circuit
+ * the app.update.interval when there isn't connectivity
+ */
+ _registerOnlineObserver: function AUS__registerOnlineObserver() {
+ if (this._registeredOnlineObserver) {
+ LOG("UpdateService:_registerOnlineObserver - observer already registered");
+ return;
+ }
+
+ LOG("UpdateService:_registerOnlineObserver - waiting for the network to " +
+ "be online, then forcing another check");
+
+ Services.obs.addObserver(this, "network:offline-status-changed", false);
+ this._registeredOnlineObserver = true;
+ },
+
+ /**
+ * Called from the network:offline-status-changed observer.
+ */
+ _offlineStatusChanged: function AUS__offlineStatusChanged(status) {
+ if (status !== "online") {
+ return;
+ }
+
+ Services.obs.removeObserver(this, "network:offline-status-changed");
+ this._registeredOnlineObserver = false;
+
+ LOG("UpdateService:_offlineStatusChanged - network is online, forcing " +
+ "another background check");
+
+ // the background checker is contained in notify
+ this._attemptResume();
+ },
+
+ onCheckComplete: function AUS_onCheckComplete(request, updates, updateCount) {
+ this._selectAndInstallUpdate(updates);
+ },
+
+ onError: function AUS_onError(request, update) {
+ LOG("UpdateService:onError - error during background update. error code: " +
+ update.errorCode + ", status text: " + update.statusText);
+
+ if (update.errorCode == NETWORK_ERROR_OFFLINE) {
+ // Register an online observer to try again
+ this._registerOnlineObserver();
+ if (this._pingSuffix) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_OFFLINE);
+ }
+ return;
+ }
+
+ // Send the error code to telemetry
+ AUSTLMY.pingCheckExError(this._pingSuffix, update.errorCode);
+ update.errorCode = BACKGROUNDCHECK_MULTIPLE_FAILURES;
+ let errCount = getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDERRORS, 0);
+ errCount++;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, errCount);
+ // Don't allow the preference to set a value greater than 20 for max errors.
+ let maxErrors = Math.min(getPref("getIntPref", PREF_APP_UPDATE_BACKGROUNDMAXERRORS, 10), 20);
+
+ if (errCount >= maxErrors) {
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(update);
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_PROMPT);
+ } else {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_GENERAL_ERROR_SILENT);
+ }
+ },
+
+ /**
+ * Called when a connection should be resumed
+ */
+ _attemptResume: function AUS_attemptResume() {
+ LOG("UpdateService:_attemptResume");
+ // If a download is in progress, then resume it.
+ if (this._downloader && this._downloader._patch &&
+ this._downloader._patch.state == STATE_DOWNLOADING &&
+ this._downloader._update) {
+ LOG("UpdateService:_attemptResume - _patch.state: " +
+ this._downloader._patch.state);
+ // Make sure downloading is the state for selectPatch to work correctly
+ writeStatusFile(getUpdatesDir(), STATE_DOWNLOADING);
+ var status = this.downloadUpdate(this._downloader._update,
+ this._downloader.background);
+ LOG("UpdateService:_attemptResume - downloadUpdate status: " + status);
+ if (status == STATE_NONE) {
+ cleanupActiveUpdate();
+ }
+ return;
+ }
+
+ this.backgroundChecker.checkForUpdates(this, false);
+ },
+
+ /**
+ * Notified when a timer fires
+ * @param timer
+ * The timer that fired
+ */
+ notify: function AUS_notify(timer) {
+ this._checkForBackgroundUpdates(true);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ checkForBackgroundUpdates: function AUS_checkForBackgroundUpdates() {
+ this._checkForBackgroundUpdates(false);
+ },
+
+ // The suffix used for background update check telemetry histogram ID's.
+ get _pingSuffix() {
+ return this._isNotify ? AUSTLMY.NOTIFY : AUSTLMY.EXTERNAL;
+ },
+
+ /**
+ * Checks for updates in the background.
+ * @param isNotify
+ * Whether or not a background update check was initiated by the
+ * application update timer notification.
+ */
+ _checkForBackgroundUpdates: function AUS__checkForBackgroundUpdates(isNotify) {
+ this._isNotify = isNotify;
+
+ // Histogram IDs:
+ // UPDATE_PING_COUNT_EXTERNAL
+ // UPDATE_PING_COUNT_NOTIFY
+ AUSTLMY.pingGeneric("UPDATE_PING_COUNT_" + this._pingSuffix,
+ true, false);
+
+ // Histogram IDs:
+ // UPDATE_UNABLE_TO_APPLY_EXTERNAL
+ // UPDATE_UNABLE_TO_APPLY_NOTIFY
+ AUSTLMY.pingGeneric("UPDATE_UNABLE_TO_APPLY_" + this._pingSuffix,
+ getCanApplyUpdates(), true);
+ // Histogram IDs:
+ // UPDATE_CANNOT_STAGE_EXTERNAL
+ // UPDATE_CANNOT_STAGE_NOTIFY
+ AUSTLMY.pingGeneric("UPDATE_CANNOT_STAGE_" + this._pingSuffix,
+ getCanStageUpdates(), true);
+ // Histogram IDs:
+ // UPDATE_INVALID_LASTUPDATETIME_EXTERNAL
+ // UPDATE_INVALID_LASTUPDATETIME_NOTIFY
+ // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_EXTERNAL
+ // UPDATE_LAST_NOTIFY_INTERVAL_DAYS_NOTIFY
+ AUSTLMY.pingLastUpdateTime(this._pingSuffix);
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_ENABLED_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_ENABLED_NOTIFY
+ AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_ENABLED_" + this._pingSuffix,
+ PREF_APP_UPDATE_ENABLED, true, true);
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_AUTO_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_AUTO_NOTIFY
+ AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_AUTO_" + this._pingSuffix,
+ PREF_APP_UPDATE_AUTO, true, true);
+ // Histogram IDs:
+ // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_EXTERNAL
+ // UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_NOTIFY
+ AUSTLMY.pingBoolPref("UPDATE_NOT_PREF_UPDATE_STAGING_ENABLED_" +
+ this._pingSuffix,
+ PREF_APP_UPDATE_STAGING_ENABLED, true, true);
+ if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ // Histogram IDs:
+ // UPDATE_PREF_UPDATE_CANCELATIONS_EXTERNAL
+ // UPDATE_PREF_UPDATE_CANCELATIONS_NOTIFY
+ AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_" + this._pingSuffix,
+ PREF_APP_UPDATE_CANCELATIONS, 0, 0);
+ }
+ if (AppConstants.platform == "macosx") {
+ // Histogram IDs:
+ // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_EXTERNAL
+ // UPDATE_PREF_UPDATE_CANCELATIONS_OSX_NOTIFY
+ AUSTLMY.pingIntPref("UPDATE_PREF_UPDATE_CANCELATIONS_OSX_" +
+ 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());
+ }
+ }
+
+ // If a download is in progress or the patch has been staged do nothing.
+ if (this.isDownloading) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADING);
+ return;
+ }
+
+ if (this._downloader && this._downloader.patchIsStaged) {
+ let readState = readStatusFile(getUpdatesDir());
+ if (readState == STATE_PENDING || readState == STATE_PENDING_SERVICE ||
+ readState == STATE_PENDING_ELEVATE) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_DOWNLOADED);
+ } else {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_IS_STAGED);
+ }
+ return;
+ }
+
+ let validUpdateURL = true;
+ try {
+ this.backgroundChecker.getUpdateURL(false);
+ } catch (e) {
+ validUpdateURL = false;
+ }
+ // The following checks are done here so they can be differentiated from
+ // foreground checks.
+ if (!UpdateUtils.OSVersion) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_OS_VERSION);
+ } 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);
+ } else if (!getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true)) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED);
+ } else if (!hasUpdateMutex()) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_MUTEX);
+ } else if (!gCanCheckForUpdates) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_CHECK);
+ } else if (!this.backgroundChecker._enabled) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DISABLED_FOR_SESSION);
+ }
+
+ this.backgroundChecker.checkForUpdates(this, false);
+ },
+
+ /**
+ * Determine the update from the specified updates that should be offered.
+ * If both valid major and minor updates are available the minor update will
+ * be offered.
+ * @param updates
+ * An array of available nsIUpdate items
+ * @return The nsIUpdate to offer.
+ */
+ selectUpdate: function AUS_selectUpdate(updates) {
+ if (updates.length == 0) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_NO_UPDATE_FOUND);
+ return null;
+ }
+
+ // The ping for unsupported is sent after the call to showPrompt.
+ if (updates.length == 1 && updates[0].unsupported) {
+ return updates[0];
+ }
+
+ // Choose the newest of the available minor and major updates.
+ var majorUpdate = null;
+ var minorUpdate = null;
+ var vc = Services.vc;
+ let lastCheckCode = AUSTLMY.CHK_NO_COMPAT_UPDATE_FOUND;
+
+ updates.forEach(function(aUpdate) {
+ // Ignore updates for older versions of the application and updates for
+ // the same version of the application with the same build ID.
+ if (vc.compare(aUpdate.appVersion, Services.appinfo.version) < 0 ||
+ vc.compare(aUpdate.appVersion, Services.appinfo.version) == 0 &&
+ aUpdate.buildID == Services.appinfo.appBuildID) {
+ LOG("UpdateService:selectUpdate - skipping update because the " +
+ "update's application version is less than the current " +
+ "application version");
+ lastCheckCode = AUSTLMY.CHK_UPDATE_PREVIOUS_VERSION;
+ return;
+ }
+
+ // Skip the update if the user responded with "never" to this update's
+ // application version and the update specifies showNeverForVersion
+ // (see bug 350636).
+ let neverPrefName = PREFBRANCH_APP_UPDATE_NEVER + aUpdate.appVersion;
+ if (aUpdate.showNeverForVersion &&
+ getPref("getBoolPref", neverPrefName, false)) {
+ LOG("UpdateService:selectUpdate - skipping update because the " +
+ "preference " + neverPrefName + " is true");
+ lastCheckCode = AUSTLMY.CHK_UPDATE_NEVER_PREF;
+ return;
+ }
+
+ switch (aUpdate.type) {
+ case "major":
+ if (!majorUpdate)
+ majorUpdate = aUpdate;
+ else if (vc.compare(majorUpdate.appVersion, aUpdate.appVersion) <= 0)
+ majorUpdate = aUpdate;
+ break;
+ case "minor":
+ if (!minorUpdate)
+ minorUpdate = aUpdate;
+ else if (vc.compare(minorUpdate.appVersion, aUpdate.appVersion) <= 0)
+ minorUpdate = aUpdate;
+ break;
+ default:
+ LOG("UpdateService:selectUpdate - skipping unknown update type: " +
+ aUpdate.type);
+ lastCheckCode = AUSTLMY.CHK_UPDATE_INVALID_TYPE;
+ break;
+ }
+ });
+
+ let update = minorUpdate || majorUpdate;
+ if (AppConstants.platform == "macosx" && update) {
+ if (getElevationRequired()) {
+ let installAttemptVersion = getPref("getCharPref",
+ PREF_APP_UPDATE_ELEVATE_VERSION,
+ null);
+ if (vc.compare(installAttemptVersion, update.appVersion) != 0) {
+ Services.prefs.setCharPref(PREF_APP_UPDATE_ELEVATE_VERSION,
+ update.appVersion);
+ if (Services.prefs.prefHasUserValue(
+ PREF_APP_UPDATE_CANCELATIONS_OSX)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
+ }
+ } else {
+ let numCancels = getPref("getIntPref",
+ PREF_APP_UPDATE_CANCELATIONS_OSX, 0);
+ let rejectedVersion = getPref("getCharPref",
+ PREF_APP_UPDATE_ELEVATE_NEVER, "");
+ let maxCancels = getPref("getIntPref",
+ PREF_APP_UPDATE_CANCELATIONS_OSX_MAX,
+ DEFAULT_CANCELATIONS_OSX_MAX);
+ if (numCancels >= maxCancels) {
+ LOG("UpdateService:selectUpdate - the user requires elevation to " +
+ "install this update, but the user has exceeded the max " +
+ "number of elevation attempts.");
+ update.elevationFailure = true;
+ AUSTLMY.pingCheckCode(
+ this._pingSuffix,
+ AUSTLMY.CHK_ELEVATION_DISABLED_FOR_VERSION);
+ } else if (vc.compare(rejectedVersion, update.appVersion) == 0) {
+ LOG("UpdateService:selectUpdate - the user requires elevation to " +
+ "install this update, but elevation is disabled for this " +
+ "version.");
+ update.elevationFailure = true;
+ AUSTLMY.pingCheckCode(this._pingSuffix,
+ AUSTLMY.CHK_ELEVATION_OPTOUT_FOR_VERSION);
+ } else {
+ LOG("UpdateService:selectUpdate - the user requires elevation to " +
+ "install the update.");
+ }
+ }
+ } else {
+ // Clear elevation-related prefs since they no longer apply (the user
+ // may have gained write access to the Firefox directory or an update
+ // was executed with a different profile).
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_VERSION)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_VERSION);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_CANCELATIONS_OSX)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_CANCELATIONS_OSX);
+ }
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ELEVATE_NEVER)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ELEVATE_NEVER);
+ }
+ }
+ } else if (!update) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, lastCheckCode);
+ }
+
+ return update;
+ },
+
+ /**
+ * Determine which of the specified updates should be installed and begin the
+ * download/installation process or notify the user about the update.
+ * @param updates
+ * An array of available updates
+ */
+ _selectAndInstallUpdate: function AUS__selectAndInstallUpdate(updates) {
+ // Return early if there's an active update. The user is already aware and
+ // is downloading or performed some user action to prevent notification.
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ if (um.activeUpdate) {
+ if (AppConstants.platform == "gonk") {
+ // For gonk, the user isn't necessarily aware of the update, so we need
+ // to show the prompt to make sure.
+ this._showPrompt(um.activeUpdate);
+ }
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_HAS_ACTIVEUPDATE);
+ return;
+ }
+
+ var updateEnabled = getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true);
+ if (!updateEnabled) {
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_PREF_DISABLED);
+ LOG("UpdateService:_selectAndInstallUpdate - not prompting because " +
+ "update is disabled");
+ return;
+ }
+
+ var update = this.selectUpdate(updates, updates.length);
+ if (!update || update.elevationFailure) {
+ return;
+ }
+
+ if (update.unsupported) {
+ LOG("UpdateService:_selectAndInstallUpdate - update not supported for " +
+ "this system");
+ if (!getPref("getBoolPref", PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, false)) {
+ LOG("UpdateService:_selectAndInstallUpdate - notifying that the " +
+ "update is not supported for this system");
+ this._showPrompt(update);
+ }
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNSUPPORTED);
+ return;
+ }
+
+ if (!getCanApplyUpdates()) {
+ LOG("UpdateService:_selectAndInstallUpdate - the user is unable to " +
+ "apply updates... prompting");
+ this._showPrompt(update);
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_UNABLE_TO_APPLY);
+ return;
+ }
+
+ /**
+ * From this point on there are two possible outcomes:
+ * 1. download and install the update automatically
+ * 2. notify the user about the availability of an update
+ *
+ * Notes:
+ * a) if the app.update.auto preference is false then automatic download and
+ * install is disabled and the user will be notified.
+ * b) if the update has a showPrompt attribute the user will be notified.
+ *
+ * If the update when it is first read does not have an appVersion attribute
+ * the following deprecated behavior will occur:
+ * Update Type Outcome
+ * Major Notify
+ * Minor Auto Install
+ */
+ if (update.showPrompt) {
+ LOG("UpdateService:_selectAndInstallUpdate - prompting because the " +
+ "update snippet specified showPrompt");
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_SNIPPET);
+ this._showPrompt(update);
+ return;
+ }
+
+ if (!getPref("getBoolPref", PREF_APP_UPDATE_AUTO, true)) {
+ LOG("UpdateService:_selectAndInstallUpdate - prompting because silent " +
+ "install is disabled");
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_SHOWPROMPT_PREF);
+ this._showPrompt(update);
+ return;
+ }
+
+ LOG("UpdateService:_selectAndInstallUpdate - download the update");
+ let status = this.downloadUpdate(update, true);
+ if (status == STATE_NONE) {
+ cleanupActiveUpdate();
+ }
+ AUSTLMY.pingCheckCode(this._pingSuffix, AUSTLMY.CHK_DOWNLOAD_UPDATE);
+ },
+
+ _showPrompt: function AUS__showPrompt(update) {
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateAvailable(update);
+ },
+
+ /**
+ * The Checker used for background update checks.
+ */
+ _backgroundChecker: null,
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get backgroundChecker() {
+ if (!this._backgroundChecker)
+ this._backgroundChecker = new Checker();
+ return this._backgroundChecker;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canCheckForUpdates() {
+ return gCanCheckForUpdates && hasUpdateMutex();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get elevationRequired() {
+ return getElevationRequired();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canApplyUpdates() {
+ return getCanApplyUpdates() && hasUpdateMutex();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get canStageUpdates() {
+ return getCanStageUpdates();
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get isOtherInstanceHandlingUpdates() {
+ return !hasUpdateMutex();
+ },
+
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ addDownloadListener: function AUS_addDownloadListener(listener) {
+ if (!this._downloader) {
+ LOG("UpdateService:addDownloadListener - no downloader!");
+ return;
+ }
+ this._downloader.addDownloadListener(listener);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ removeDownloadListener: function AUS_removeDownloadListener(listener) {
+ if (!this._downloader) {
+ LOG("UpdateService:removeDownloadListener - no downloader!");
+ return;
+ }
+ this._downloader.removeDownloadListener(listener);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ downloadUpdate: function AUS_downloadUpdate(update, background) {
+ if (!update)
+ throw Cr.NS_ERROR_NULL_POINTER;
+
+ // Don't download the update if the update's version is less than the
+ // current application's version or the update's version is the same as the
+ // application's version and the build ID is the same as the application's
+ // build ID.
+ if (update.appVersion &&
+ (Services.vc.compare(update.appVersion, Services.appinfo.version) < 0 ||
+ update.buildID && update.buildID == Services.appinfo.appBuildID &&
+ update.appVersion == Services.appinfo.version)) {
+ LOG("UpdateService:downloadUpdate - canceling download of update since " +
+ "it is for an earlier or same application version and build ID.\n" +
+ "current application version: " + Services.appinfo.version + "\n" +
+ "update application version : " + update.appVersion + "\n" +
+ "current build ID: " + Services.appinfo.appBuildID + "\n" +
+ "update build ID : " + update.buildID);
+ cleanupActiveUpdate();
+ return STATE_NONE;
+ }
+
+ // If a download request is in progress vs. a download ready to resume
+ if (this.isDownloading) {
+ if (update.isCompleteUpdate == this._downloader.isCompleteUpdate &&
+ background == this._downloader.background) {
+ LOG("UpdateService:downloadUpdate - no support for downloading more " +
+ "than one update at a time");
+ return readStatusFile(getUpdatesDir());
+ }
+ this._downloader.cancel();
+ }
+ if (AppConstants.platform == "gonk") {
+ let um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ let activeUpdate = um.activeUpdate;
+ if (activeUpdate &&
+ (activeUpdate.appVersion != update.appVersion ||
+ activeUpdate.buildID != update.buildID)) {
+ // We have an activeUpdate (which presumably was interrupted), and are
+ // about start downloading a new one. Make sure we remove all traces
+ // of the active one (otherwise we'll start appending the new update.mar
+ // the the one that's been partially downloaded).
+ LOG("UpdateService:downloadUpdate - removing stale active update.");
+ cleanupActiveUpdate();
+ }
+ }
+ // Set the previous application version prior to downloading the update.
+ update.previousAppVersion = Services.appinfo.version;
+ this._downloader = new Downloader(background, this);
+ return this._downloader.downloadUpdate(update);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ pauseDownload: function AUS_pauseDownload() {
+ if (this.isDownloading) {
+ this._downloader.cancel();
+ } else if (this._retryTimer) {
+ // Download status is still consider as 'downloading' during retry.
+ // We need to cancel both retry and download at this stage.
+ this._retryTimer.cancel();
+ this._retryTimer = null;
+ this._downloader.cancel();
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getUpdatesDirectory: getUpdatesDir,
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get isDownloading() {
+ return this._downloader && this._downloader.isBusy;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ applyOsUpdate: function AUS_applyOsUpdate(aUpdate) {
+ if (!aUpdate.isOSUpdate || aUpdate.state != STATE_APPLIED) {
+ aUpdate.statusText = "fota-state-error";
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ aUpdate.QueryInterface(Ci.nsIWritablePropertyBag);
+ let osApplyToDir = aUpdate.getProperty("osApplyToDir");
+
+ if (!osApplyToDir) {
+ LOG("UpdateService:applyOsUpdate - Error: osApplyToDir is not defined" +
+ "in the nsIUpdate!");
+ pingStateAndStatusCodes(aUpdate, false,
+ STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR);
+ handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR);
+ return;
+ }
+
+ let updateFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ updateFile.initWithPath(osApplyToDir + "/update.zip");
+ if (!updateFile.exists()) {
+ LOG("UpdateService:applyOsUpdate - Error: OS update is not found at " +
+ updateFile.path);
+ pingStateAndStatusCodes(aUpdate, false,
+ STATE_FAILED + ": " + FOTA_FILE_OPERATION_ERROR);
+ handleUpdateFailure(aUpdate, FOTA_FILE_OPERATION_ERROR);
+ return;
+ }
+
+ writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED_OS);
+ LOG("UpdateService:applyOsUpdate - Rebooting into recovery to apply " +
+ "FOTA update: " + updateFile.path);
+ try {
+ let recoveryService = Cc["@mozilla.org/recovery-service;1"]
+ .getService(Ci.nsIRecoveryService);
+ recoveryService.installFotaUpdate(updateFile.path);
+ } catch (e) {
+ LOG("UpdateService:applyOsUpdate - Error: Couldn't reboot into recovery" +
+ " to apply FOTA update " + updateFile.path);
+ pingStateAndStatusCodes(aUpdate, false,
+ STATE_FAILED + ": " + FOTA_RECOVERY_ERROR);
+ writeStatusFile(getUpdatesDir(), aUpdate.state = STATE_APPLIED);
+ handleUpdateFailure(aUpdate, FOTA_RECOVERY_ERROR);
+ }
+ },
+
+ classID: UPDATESERVICE_CID,
+ classInfo: XPCOMUtils.generateCI({classID: UPDATESERVICE_CID,
+ contractID: UPDATESERVICE_CONTRACTID,
+ interfaces: [Ci.nsIApplicationUpdateService,
+ Ci.nsITimerCallback,
+ Ci.nsIObserver],
+ flags: Ci.nsIClassInfo.SINGLETON}),
+
+ _xpcom_factory: UpdateServiceFactory,
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIApplicationUpdateService,
+ Ci.nsIUpdateCheckListener,
+ Ci.nsITimerCallback,
+ Ci.nsIObserver])
+};
+
+/**
+ * A service to manage active and past updates.
+ * @constructor
+ */
+function UpdateManager() {
+ // Ensure the Active Update file is loaded
+ var updates = this._loadXMLFileIntoArray(getUpdateFile(
+ [FILE_ACTIVE_UPDATE_XML]));
+ if (updates.length > 0) {
+ // Under some edgecases such as Windows system restore the active-update.xml
+ // will contain a pending update without the status file which will return
+ // STATE_NONE. To recover from this situation clean the updates dir and
+ // rewrite the active-update.xml file without the broken update.
+ if (readStatusFile(getUpdatesDir()) == STATE_NONE) {
+ cleanUpUpdatesDir();
+ this._writeUpdatesToXMLFile([], getUpdateFile([FILE_ACTIVE_UPDATE_XML]));
+ }
+ else
+ this._activeUpdate = updates[0];
+ }
+}
+UpdateManager.prototype = {
+ /**
+ * All previously downloaded and installed updates, as an array of nsIUpdate
+ * objects.
+ */
+ _updates: null,
+
+ /**
+ * The current actively downloading/installing update, as a nsIUpdate object.
+ */
+ _activeUpdate: null,
+
+ /**
+ * Handle Observer Service notifications
+ * @param subject
+ * The subject of the notification
+ * @param topic
+ * The notification name
+ * @param data
+ * Additional data
+ */
+ observe: function UM_observe(subject, topic, data) {
+ // Hack to be able to run and cleanup tests by reloading the update data.
+ if (topic == "um-reload-update-data") {
+ this._updates = this._loadXMLFileIntoArray(getUpdateFile(
+ [FILE_UPDATES_XML]));
+ this._activeUpdate = null;
+ var updates = this._loadXMLFileIntoArray(getUpdateFile(
+ [FILE_ACTIVE_UPDATE_XML]));
+ if (updates.length > 0)
+ this._activeUpdate = updates[0];
+ }
+ },
+
+ /**
+ * Loads an updates.xml formatted file into an array of nsIUpdate items.
+ * @param file
+ * A nsIFile for the updates.xml file
+ * @return The array of nsIUpdate items held in the file.
+ */
+ _loadXMLFileIntoArray: function UM__loadXMLFileIntoArray(file) {
+ if (!file.exists()) {
+ LOG("UpdateManager:_loadXMLFileIntoArray: XML file does not exist");
+ return [];
+ }
+
+ var result = [];
+ var fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fileStream.init(file, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ try {
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ var doc = parser.parseFromStream(fileStream, "UTF-8",
+ fileStream.available(), "text/xml");
+
+ const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+ var updateCount = doc.documentElement.childNodes.length;
+ for (var i = 0; i < updateCount; ++i) {
+ var updateElement = doc.documentElement.childNodes.item(i);
+ if (updateElement.nodeType != ELEMENT_NODE ||
+ updateElement.localName != "update")
+ continue;
+
+ updateElement.QueryInterface(Ci.nsIDOMElement);
+ let update;
+ try {
+ update = new Update(updateElement);
+ } catch (e) {
+ LOG("UpdateManager:_loadXMLFileIntoArray - invalid update");
+ continue;
+ }
+ result.push(update);
+ }
+ }
+ catch (e) {
+ LOG("UpdateManager:_loadXMLFileIntoArray - error constructing update " +
+ "list. Exception: " + e);
+ }
+ fileStream.close();
+ return result;
+ },
+
+ /**
+ * Load the update manager, initializing state from state files.
+ */
+ _ensureUpdates: function UM__ensureUpdates() {
+ if (!this._updates) {
+ this._updates = this._loadXMLFileIntoArray(getUpdateFile(
+ [FILE_UPDATES_XML]));
+ var activeUpdates = this._loadXMLFileIntoArray(getUpdateFile(
+ [FILE_ACTIVE_UPDATE_XML]));
+ if (activeUpdates.length > 0)
+ this._activeUpdate = activeUpdates[0];
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ getUpdateAt: function UM_getUpdateAt(index) {
+ this._ensureUpdates();
+ return this._updates[index];
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get updateCount() {
+ this._ensureUpdates();
+ return this._updates.length;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ get activeUpdate() {
+ if (this._activeUpdate &&
+ this._activeUpdate.channel != UpdateUtils.UpdateChannel) {
+ LOG("UpdateManager:get activeUpdate - channel has changed, " +
+ "reloading default preferences to workaround bug 802022");
+ // Workaround to get distribution preferences loaded (Bug 774618). This
+ // can be removed after bug 802022 is fixed.
+ let prefSvc = Services.prefs.QueryInterface(Ci.nsIObserver);
+ prefSvc.observe(null, "reload-default-prefs", null);
+ if (this._activeUpdate.channel != UpdateUtils.UpdateChannel) {
+ // User switched channels, clear out any old active updates and remove
+ // partial downloads
+ this._activeUpdate = null;
+ this.saveUpdates();
+
+ // Destroy the updates directory, since we're done with it.
+ cleanUpUpdatesDir();
+ }
+ }
+ return this._activeUpdate;
+ },
+ set activeUpdate(activeUpdate) {
+ this._addUpdate(activeUpdate);
+ this._activeUpdate = activeUpdate;
+ if (!activeUpdate) {
+ // If |activeUpdate| is null, we have updated both lists - the active list
+ // and the history list, so we want to write both files.
+ this.saveUpdates();
+ }
+ else
+ this._writeUpdatesToXMLFile([this._activeUpdate],
+ getUpdateFile([FILE_ACTIVE_UPDATE_XML]));
+ return activeUpdate;
+ },
+
+ /**
+ * Add an update to the Updates list. If the item already exists in the list,
+ * replace the existing value with the new value.
+ * @param update
+ * The nsIUpdate object to add.
+ */
+ _addUpdate: function UM__addUpdate(update) {
+ if (!update)
+ return;
+ this._ensureUpdates();
+ if (this._updates) {
+ for (var i = 0; i < this._updates.length; ++i) {
+ // Keep all update entries with a state of STATE_FAILED and replace the
+ // first update entry that has the same application version and build ID
+ // if it exists. This allows the update history to only have one success
+ // entry for an update and entries for all failed updates.
+ if (update.state != STATE_FAILED &&
+ this._updates[i] &&
+ this._updates[i].state != STATE_FAILED &&
+ this._updates[i].appVersion == update.appVersion &&
+ this._updates[i].buildID == update.buildID) {
+ // Replace the existing entry with the new value, updating
+ // all metadata.
+ this._updates[i] = update;
+ return;
+ }
+ }
+ }
+ // Otherwise add it to the front of the list.
+ this._updates.unshift(update);
+ },
+
+ /**
+ * Serializes an array of updates to an XML file
+ * @param updates
+ * An array of nsIUpdate objects
+ * @param file
+ * The nsIFile object to serialize to
+ */
+ _writeUpdatesToXMLFile: function UM__writeUpdatesToXMLFile(updates, file) {
+ var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ var modeFlags = FileUtils.MODE_WRONLY | FileUtils.MODE_CREATE |
+ FileUtils.MODE_TRUNCATE;
+ if (!file.exists()) {
+ file.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, FileUtils.PERMS_FILE);
+ }
+ fos.init(file, modeFlags, FileUtils.PERMS_FILE, 0);
+
+ try {
+ var parser = Cc["@mozilla.org/xmlextras/domparser;1"].
+ createInstance(Ci.nsIDOMParser);
+ const EMPTY_UPDATES_DOCUMENT = "<?xml version=\"1.0\"?><updates xmlns=\"http://www.mozilla.org/2005/app-update\"></updates>";
+ var doc = parser.parseFromString(EMPTY_UPDATES_DOCUMENT, "text/xml");
+
+ for (var i = 0; i < updates.length; ++i) {
+ // If appVersion isn't defined don't add the update. This happens when
+ // cleaning up invalid updates (e.g. incorrect channel).
+ if (updates[i] && updates[i].appVersion) {
+ doc.documentElement.appendChild(updates[i].serialize(doc));
+ }
+ }
+
+ var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
+ createInstance(Ci.nsIDOMSerializer);
+ serializer.serializeToStream(doc.documentElement, fos, null);
+ } catch (e) {
+ }
+
+ FileUtils.closeSafeFileOutputStream(fos);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ saveUpdates: function UM_saveUpdates() {
+ this._writeUpdatesToXMLFile([this._activeUpdate],
+ getUpdateFile([FILE_ACTIVE_UPDATE_XML]));
+ if (this._activeUpdate)
+ this._addUpdate(this._activeUpdate);
+
+ this._ensureUpdates();
+ // Don't write updates that have a temporary state to the updates.xml file.
+ if (this._updates) {
+ let updates = this._updates.slice();
+ 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_PENDING_ELEVATE) {
+ updates.splice(i, 1);
+ }
+ }
+
+ this._writeUpdatesToXMLFile(updates.slice(0, 20),
+ getUpdateFile([FILE_UPDATES_XML]));
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ refreshUpdateStatus: function UM_refreshUpdateStatus() {
+ var update = this._activeUpdate;
+ if (!update) {
+ return;
+ }
+ var status = readStatusFile(getUpdatesDir());
+ pingStateAndStatusCodes(update, false, status);
+ var parts = status.split(":");
+ update.state = parts[0];
+ if (update.state == STATE_FAILED && parts[1]) {
+ update.errorCode = parseInt(parts[1]);
+ }
+ let um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ // Save a copy of the active update's current state to the updates.xml so
+ // it is possible to track failures.
+ um.saveUpdates();
+
+ // Rotate the update logs so the update log isn't removed if a complete
+ // update is downloaded. By passing false the patch directory won't be
+ // removed.
+ cleanUpUpdatesDir(false);
+
+ if (update.state == STATE_FAILED && parts[1]) {
+ if (!handleUpdateFailure(update, parts[1])) {
+ handleFallbackToCompleteUpdate(update, true);
+ }
+
+ 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.
+ LOG("UpdateManager:refreshUpdateStatus - Notifying observers that " +
+ "the update was staged. state: " + update.state + ", status: " + status);
+ Services.obs.notifyObservers(null, "update-staged", update.state);
+
+ if (AppConstants.platform == "gonk") {
+ // Do this after everything else, since it will likely cause the app to
+ // shut down.
+ if (update.state == STATE_APPLIED) {
+ // Notify the user that an update has been staged and is ready for
+ // installation (i.e. that they should restart the application). We do
+ // not notify on failed update attempts.
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateDownloaded(update, true);
+ } else {
+ releaseSDCardMountLock();
+ }
+ return;
+ }
+ // Only prompt when the UI isn't already open.
+ let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null);
+ if (Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME) ||
+ windowType && Services.wm.getMostRecentWindow(windowType)) {
+ return;
+ }
+
+ 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).
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateDownloaded(update, true);
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ elevationOptedIn: function UM_elevationOptedIn() {
+ // The user has been been made aware that the update requires elevation.
+ let update = this._activeUpdate;
+ if (!update) {
+ return;
+ }
+ let status = readStatusFile(getUpdatesDir());
+ let parts = status.split(":");
+ update.state = parts[0];
+ if (update.state == STATE_PENDING_ELEVATE) {
+ // Proceed with the pending update.
+ // Note: STATE_PENDING_ELEVATE stands for "pending user's approval to
+ // proceed with an elevated update". As long as we see this state, we will
+ // notify the user of the availability of an update that requires
+ // elevation. |elevationOptedIn| (this function) is called when the user
+ // gives us approval to proceed, so we want to switch to STATE_PENDING.
+ // The updater then detects whether or not elevation is required and
+ // displays the elevation prompt if necessary. This last step does not
+ // depend on the state in the status file.
+ writeStatusFile(getUpdatesDir(), STATE_PENDING);
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ cleanupActiveUpdate: function UM_cleanupActiveUpdate() {
+ cleanupActiveUpdate();
+ },
+
+ classID: Components.ID("{093C2356-4843-4C65-8709-D7DBCBBE7DFB}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateManager, Ci.nsIObserver])
+};
+
+/**
+ * Checker
+ * Checks for new Updates
+ * @constructor
+ */
+function Checker() {
+}
+Checker.prototype = {
+ /**
+ * The XMLHttpRequest object that performs the connection.
+ */
+ _request: null,
+
+ /**
+ * The nsIUpdateCheckListener callback
+ */
+ _callback: null,
+
+ /**
+ * The URL of the update service XML file to connect to that contains details
+ * about available updates.
+ */
+ getUpdateURL: function UC_getUpdateURL(force) {
+ this._forced = force;
+
+ let url;
+ try {
+ url = Services.prefs.getDefaultBranch(null).
+ getCharPref(PREF_APP_UPDATE_URL);
+ } catch (e) {
+ }
+
+ if (!url || url == "") {
+ LOG("Checker:getUpdateURL - update URL not defined");
+ return null;
+ }
+
+ url = UpdateUtils.formatUpdateURL(url);
+
+ if (force) {
+ url += (url.indexOf("?") != -1 ? "&" : "?") + "force=1";
+ }
+
+ LOG("Checker:getUpdateURL - update URL: " + url);
+ return url;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ checkForUpdates: function UC_checkForUpdates(listener, force) {
+ LOG("Checker: checkForUpdates, force: " + force);
+ if (!listener)
+ throw Cr.NS_ERROR_NULL_POINTER;
+
+ Services.obs.notifyObservers(null, "update-check-start", null);
+
+ var url = this.getUpdateURL(force);
+ if (!url || (!this.enabled && !force))
+ return;
+
+ this._request = new XMLHttpRequest();
+ this._request.open("GET", url, true);
+ this._request.channel.notificationCallbacks = new gCertUtils.BadCertHandler(false);
+ // 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;
+ // Disable cutting edge features, like TLS 1.3, where middleboxes might brick us
+ this._request.channel.QueryInterface(Ci.nsIHttpChannelInternal).beConservative = true;
+
+ 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");
+
+ var self = this;
+ this._request.addEventListener("error", function(event) { self.onError(event); }, false);
+ this._request.addEventListener("load", function(event) { self.onLoad(event); }, false);
+
+ LOG("Checker:checkForUpdates - sending request to: " + url);
+ this._request.send(null);
+
+ this._callback = listener;
+ },
+
+ /**
+ * Returns an array of nsIUpdate objects discovered by the update check.
+ * @throws if the XML document element node name is not updates.
+ */
+ get _updates() {
+ var updatesElement = this._request.responseXML.documentElement;
+ if (!updatesElement) {
+ LOG("Checker:_updates get - empty updates document?!");
+ return [];
+ }
+
+ if (updatesElement.nodeName != "updates") {
+ LOG("Checker:_updates get - unexpected node name!");
+ throw new Error("Unexpected node name, expected: updates, got: " +
+ updatesElement.nodeName);
+ }
+
+ const ELEMENT_NODE = Ci.nsIDOMNode.ELEMENT_NODE;
+ var updates = [];
+ for (var i = 0; i < updatesElement.childNodes.length; ++i) {
+ var updateElement = updatesElement.childNodes.item(i);
+ if (updateElement.nodeType != ELEMENT_NODE ||
+ updateElement.localName != "update")
+ continue;
+
+ updateElement.QueryInterface(Ci.nsIDOMElement);
+ let update;
+ try {
+ update = new Update(updateElement);
+ } catch (e) {
+ LOG("Checker:_updates get - invalid <update/>, ignoring...");
+ continue;
+ }
+ update.serviceURL = this.getUpdateURL(this._forced);
+ update.channel = UpdateUtils.UpdateChannel;
+ updates.push(update);
+ }
+
+ return updates;
+ },
+
+ /**
+ * Returns the status code for the XMLHttpRequest
+ */
+ _getChannelStatus: function UC__getChannelStatus(request) {
+ var status = 0;
+ try {
+ status = request.status;
+ }
+ catch (e) {
+ }
+
+ if (status == 0)
+ status = request.channel.QueryInterface(Ci.nsIRequest).status;
+ return status;
+ },
+
+ _isHttpStatusCode: function UC__isHttpStatusCode(status) {
+ return status >= 100 && status <= 599;
+ },
+
+ /**
+ * The XMLHttpRequest succeeded and the document was loaded.
+ * @param event
+ * The nsIDOMEvent for the load
+ */
+ onLoad: function UC_onLoad(event) {
+ LOG("Checker:onLoad - request completed downloading document");
+
+ try {
+ // Analyze the resulting DOM and determine the set of updates.
+ var updates = this._updates;
+ LOG("Checker:onLoad - number of updates available: " + updates.length);
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
+ }
+
+ // Tell the callback about the updates
+ this._callback.onCheckComplete(event.target, updates, updates.length);
+ } catch (e) {
+ LOG("Checker:onLoad - there was a problem checking for updates. " +
+ "Exception: " + e);
+ var request = event.target;
+ var status = this._getChannelStatus(request);
+ LOG("Checker:onLoad - request.status: " + status);
+ var update = new Update(null);
+ update.errorCode = status;
+ update.statusText = getStatusTextFromCode(status, 404);
+
+ if (this._isHttpStatusCode(status)) {
+ update.errorCode = HTTP_ERROR_OFFSET + status;
+ }
+
+ this._callback.onError(request, update);
+ }
+
+ this._callback = null;
+ this._request = null;
+ },
+
+ /**
+ * There was an error of some kind during the XMLHttpRequest
+ * @param event
+ * The nsIDOMEvent for the error
+ */
+ onError: function UC_onError(event) {
+ var request = event.target;
+ var status = this._getChannelStatus(request);
+ LOG("Checker:onError - request.status: " + status);
+
+ // If we can't find an error string specific to this status code,
+ // just use the 200 message from above, which means everything
+ // "looks" fine but there was probably an XML error or a bogus file.
+ var update = new Update(null);
+ update.errorCode = status;
+ update.statusText = getStatusTextFromCode(status, 200);
+
+ if (status == Cr.NS_ERROR_OFFLINE) {
+ // We use a separate constant here because nsIUpdate.errorCode is signed
+ update.errorCode = NETWORK_ERROR_OFFLINE;
+ } else if (this._isHttpStatusCode(status)) {
+ update.errorCode = HTTP_ERROR_OFFSET + status;
+ }
+
+ this._callback.onError(request, update);
+ this._request = null;
+ },
+
+ /**
+ * Whether or not we are allowed to do update checking.
+ */
+ _enabled: true,
+ get enabled() {
+ return getPref("getBoolPref", PREF_APP_UPDATE_ENABLED, true) &&
+ gCanCheckForUpdates && hasUpdateMutex() && this._enabled;
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ stopChecking: function UC_stopChecking(duration) {
+ // Always stop the current check
+ if (this._request)
+ this._request.abort();
+
+ switch (duration) {
+ case Ci.nsIUpdateChecker.CURRENT_SESSION:
+ this._enabled = false;
+ break;
+ case Ci.nsIUpdateChecker.ANY_CHECKS:
+ this._enabled = false;
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, this._enabled);
+ break;
+ }
+
+ this._callback = null;
+ },
+
+ classID: Components.ID("{898CDC9B-E43F-422F-9CC4-2F6291B415A3}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateChecker])
+};
+
+/**
+ * Manages the download of updates
+ * @param background
+ * Whether or not this downloader is operating in background
+ * update mode.
+ * @param updateService
+ * The update service that created this downloader.
+ * @constructor
+ */
+function Downloader(background, updateService) {
+ LOG("Creating Downloader");
+ this.background = background;
+ this.updateService = updateService;
+}
+Downloader.prototype = {
+ /**
+ * The nsIUpdatePatch that we are downloading
+ */
+ _patch: null,
+
+ /**
+ * The nsIUpdate that we are downloading
+ */
+ _update: null,
+
+ /**
+ * The nsIIncrementalDownload object handling the download
+ */
+ _request: null,
+
+ /**
+ * Whether or not the update being downloaded is a complete replacement of
+ * the user's existing installation or a patch representing the difference
+ * between the new version and the previous version.
+ */
+ isCompleteUpdate: null,
+
+ /**
+ * Cancels the active download.
+ */
+ cancel: function Downloader_cancel(cancelError) {
+ LOG("Downloader: cancel");
+ if (cancelError === undefined) {
+ cancelError = Cr.NS_BINDING_ABORTED;
+ }
+ if (this._request && this._request instanceof Ci.nsIRequest) {
+ this._request.cancel(cancelError);
+ }
+ if (AppConstants.platform == "gonk") {
+ releaseSDCardMountLock();
+ }
+ },
+
+ /**
+ * Whether or not a patch has been downloaded and staged for installation.
+ */
+ get patchIsStaged() {
+ var readState = readStatusFile(getUpdatesDir());
+ // 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 ||
+ readState == STATE_PENDING_ELEVATE ||
+ readState == STATE_APPLIED || readState == STATE_APPLIED_SERVICE;
+ },
+
+ /**
+ * Verify the downloaded file. We assume that the download is complete at
+ * this point.
+ */
+ _verifyDownload: function Downloader__verifyDownload() {
+ LOG("Downloader:_verifyDownload called");
+ if (!this._request) {
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_VERIFY_NO_REQUEST);
+ return false;
+ }
+
+ let destination = this._request.destination;
+
+ // Ensure that the file size matches the expected file size.
+ if (destination.fileSize != this._patch.size) {
+ LOG("Downloader:_verifyDownload downloaded size != expected size.");
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_VERIFY_PATCH_SIZE_NOT_EQUAL);
+ return false;
+ }
+
+ LOG("Downloader:_verifyDownload downloaded size == expected size.");
+
+ // The hash check is not necessary when mar signatures are used to verify
+ // the downloaded mar file.
+ if (AppConstants.MOZ_VERIFY_MAR_SIGNATURE) {
+ return true;
+ }
+
+ let fileStream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fileStream.init(destination, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+
+ let digest;
+ try {
+ let hash = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ var hashFunction = Ci.nsICryptoHash[this._patch.hashFunction.toUpperCase()];
+ if (hashFunction == undefined) {
+ throw Cr.NS_ERROR_UNEXPECTED;
+ }
+ hash.init(hashFunction);
+ hash.updateFromStream(fileStream, -1);
+ // NOTE: For now, we assume that the format of _patch.hashValue is hex
+ // encoded binary (such as what is typically output by programs like
+ // sha1sum). In the future, this may change to base64 depending on how
+ // we choose to compute these hashes.
+ digest = binaryToHex(hash.finish(false));
+ } catch (e) {
+ LOG("Downloader:_verifyDownload - failed to compute hash of the " +
+ "downloaded update archive");
+ digest = "";
+ }
+
+ fileStream.close();
+
+ if (digest == this._patch.hashValue.toLowerCase()) {
+ LOG("Downloader:_verifyDownload hashes match.");
+ return true;
+ }
+
+ LOG("Downloader:_verifyDownload hashes do not match. ");
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_VERIFY_NO_HASH_MATCH);
+ return false;
+ },
+
+ /**
+ * Select the patch to use given the current state of updateDir and the given
+ * set of update patches.
+ * @param update
+ * A nsIUpdate object to select a patch from
+ * @param updateDir
+ * A nsIFile representing the update directory
+ * @return A nsIUpdatePatch object to download
+ */
+ _selectPatch: function Downloader__selectPatch(update, updateDir) {
+ // Given an update to download, we will always try to download the patch
+ // for a partial update over the patch for a full update.
+
+ /**
+ * Return the first UpdatePatch with the given type.
+ * @param type
+ * The type of the patch ("complete" or "partial")
+ * @return A nsIUpdatePatch object matching the type specified
+ */
+ function getPatchOfType(type) {
+ for (var i = 0; i < update.patchCount; ++i) {
+ var patch = update.getPatchAt(i);
+ if (patch && patch.type == type)
+ return patch;
+ }
+ return null;
+ }
+
+ // Look to see if any of the patches in the Update object has been
+ // pre-selected for download, otherwise we must figure out which one
+ // to select ourselves.
+ var selectedPatch = update.selectedPatch;
+
+ var state = readStatusFile(updateDir);
+
+ // If this is a patch that we know about, then select it. If it is a patch
+ // that we do not know about, then remove it and use our default logic.
+ var useComplete = false;
+ if (selectedPatch) {
+ LOG("Downloader:_selectPatch - found existing patch with state: " +
+ state);
+ if (state == STATE_DOWNLOADING) {
+ LOG("Downloader:_selectPatch - resuming download");
+ return selectedPatch;
+ }
+
+ if (AppConstants.platform == "gonk") {
+ if (state == STATE_PENDING || state == STATE_APPLYING) {
+ LOG("Downloader:_selectPatch - resuming interrupted apply");
+ return selectedPatch;
+ }
+ if (state == STATE_APPLIED) {
+ LOG("Downloader:_selectPatch - already downloaded and staged");
+ return null;
+ }
+ } else if (state == STATE_PENDING || state == STATE_PENDING_SERVICE ||
+ state == STATE_PENDING_ELEVATE) {
+ LOG("Downloader:_selectPatch - already downloaded and staged");
+ return null;
+ }
+
+ if (update && selectedPatch.type == "complete") {
+ // This is a pretty fatal error. Just bail.
+ LOG("Downloader:_selectPatch - failed to apply complete patch!");
+ writeStatusFile(updateDir, STATE_NONE);
+ writeVersionFile(getUpdatesDir(), null);
+ return null;
+ }
+
+ // Something went wrong when we tried to apply the previous patch.
+ // Try the complete patch next time.
+ useComplete = true;
+ selectedPatch = null;
+ }
+
+ // If we were not able to discover an update from a previous download, we
+ // select the best patch from the given set.
+ var partialPatch = getPatchOfType("partial");
+ if (!useComplete)
+ selectedPatch = partialPatch;
+ if (!selectedPatch) {
+ if (partialPatch)
+ partialPatch.selected = false;
+ selectedPatch = getPatchOfType("complete");
+ }
+
+ // Restore the updateDir since we may have deleted it.
+ updateDir = getUpdatesDir();
+
+ // if update only contains a partial patch, selectedPatch == null here if
+ // the partial patch has been attempted and fails and we're trying to get a
+ // complete patch
+ if (selectedPatch)
+ selectedPatch.selected = true;
+
+ update.isCompleteUpdate = useComplete;
+
+ // Reset the Active Update object on the Update Manager and flush the
+ // Active Update DB.
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ um.activeUpdate = update;
+
+ return selectedPatch;
+ },
+
+ /**
+ * Whether or not we are currently downloading something.
+ */
+ get isBusy() {
+ return this._request != null;
+ },
+
+ /**
+ * Get the nsIFile to use for downloading the active update's selected patch
+ */
+ _getUpdateArchiveFile: function Downloader__getUpdateArchiveFile() {
+ var updateArchive;
+ if (AppConstants.platform == "gonk") {
+ try {
+ updateArchive = FileUtils.getDir(KEY_UPDATE_ARCHIVE_DIR, [], true);
+ } catch (e) {
+ return null;
+ }
+ } else {
+ updateArchive = getUpdatesDir().clone();
+ }
+
+ updateArchive.append(FILE_UPDATE_MAR);
+ return updateArchive;
+ },
+
+ /**
+ * Download and stage the given update.
+ * @param update
+ * A nsIUpdate object to download a patch for. Cannot be null.
+ */
+ downloadUpdate: function Downloader_downloadUpdate(update) {
+ LOG("UpdateService:_downloadUpdate");
+ if (!update) {
+ AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE);
+ throw Cr.NS_ERROR_NULL_POINTER;
+ }
+
+ var updateDir = getUpdatesDir();
+
+ this._update = update;
+
+ // This function may return null, which indicates that there are no patches
+ // to download.
+ this._patch = this._selectPatch(update, updateDir);
+ if (!this._patch) {
+ LOG("Downloader:downloadUpdate - no patch to download");
+ AUSTLMY.pingDownloadCode(undefined, AUSTLMY.DWNLD_ERR_NO_UPDATE_PATCH);
+ return readStatusFile(updateDir);
+ }
+ this.isCompleteUpdate = this._patch.type == "complete";
+
+ let patchFile = null;
+
+ // Only used by gonk
+ let status = STATE_NONE;
+ if (AppConstants.platform == "gonk") {
+ status = readStatusFile(updateDir);
+ if (isInterruptedUpdate(status)) {
+ LOG("Downloader:downloadUpdate - interruptted update");
+ // The update was interrupted. Try to locate the existing patch file.
+ // For an interrupted download, this allows a resume rather than a
+ // re-download.
+ patchFile = getFileFromUpdateLink(updateDir);
+ if (!patchFile) {
+ // No link file. We'll just assume that the update.mar is in the
+ // update directory.
+ patchFile = updateDir.clone();
+ patchFile.append(FILE_UPDATE_MAR);
+ }
+ if (patchFile.exists()) {
+ LOG("Downloader:downloadUpdate - resuming with patchFile " + patchFile.path);
+ if (patchFile.fileSize == this._patch.size) {
+ LOG("Downloader:downloadUpdate - patchFile appears to be fully downloaded");
+ // Bump the status along so that we don't try to redownload again.
+ if (getElevationRequired()) {
+ status = STATE_PENDING_ELEVATE;
+ } else {
+ status = STATE_PENDING;
+ }
+ }
+ } else {
+ LOG("Downloader:downloadUpdate - patchFile " + patchFile.path +
+ " doesn't exist - performing full download");
+ // The patchfile doesn't exist, we might as well treat this like
+ // a new download.
+ patchFile = null;
+ }
+ if (patchFile && status != STATE_DOWNLOADING) {
+ // It looks like the patch was downloaded, but got interrupted while it
+ // was being verified or applied. So we'll fake the downloading portion.
+
+ if (getElevationRequired()) {
+ writeStatusFile(updateDir, STATE_PENDING_ELEVATE);
+ } else {
+ writeStatusFile(updateDir, STATE_PENDING);
+ }
+
+ // Since the code expects the onStopRequest callback to happen
+ // asynchronously (And you have to call AUS_addDownloadListener
+ // after calling AUS_downloadUpdate) we need to defer this.
+
+ this._downloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this._downloadTimer.initWithCallback(function() {
+ this._downloadTimer = null;
+ // Send a fake onStopRequest. Filling in the destination allows
+ // _verifyDownload to work, and then the update will be applied.
+ this._request = {destination: patchFile};
+ this.onStopRequest(this._request, null, Cr.NS_OK);
+ }.bind(this), 0, Ci.nsITimer.TYPE_ONE_SHOT);
+
+ // Returning STATE_DOWNLOADING makes UpdatePrompt think we're
+ // downloading. The onStopRequest that we spoofed above will make it
+ // look like the download finished.
+ return STATE_DOWNLOADING;
+ }
+ }
+ }
+
+ if (!patchFile) {
+ // Find a place to put the patchfile that we're going to download.
+ patchFile = this._getUpdateArchiveFile();
+ }
+ if (!patchFile) {
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_NO_PATCH_FILE);
+ return STATE_NONE;
+ }
+
+ if (AppConstants.platform == "gonk") {
+ if (patchFile.path.indexOf(updateDir.path) != 0) {
+ // The patchFile is in a directory which is different from the
+ // updateDir, create a link file.
+ writeLinkFile(updateDir, patchFile);
+
+ if (!isInterruptedUpdate(status) && patchFile.exists()) {
+ // Remove stale patchFile
+ patchFile.remove(false);
+ }
+ }
+ }
+
+ update.QueryInterface(Ci.nsIPropertyBag);
+ let interval = this.background ? update.getProperty("backgroundInterval")
+ : DOWNLOAD_FOREGROUND_INTERVAL;
+
+ var uri = Services.io.newURI(this._patch.URL, null, null);
+ LOG("Downloader:downloadUpdate - url: " + uri.spec + ", path: " +
+ patchFile.path + ", interval: " + interval);
+
+ this._request = Cc["@mozilla.org/network/incremental-download;1"].
+ createInstance(Ci.nsIIncrementalDownload);
+ this._request.init(uri, patchFile, DOWNLOAD_CHUNK_SIZE, interval);
+ this._request.start(this, null);
+
+ writeStatusFile(updateDir, STATE_DOWNLOADING);
+ this._patch.QueryInterface(Ci.nsIWritablePropertyBag);
+ this._patch.state = STATE_DOWNLOADING;
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ um.saveUpdates();
+ return STATE_DOWNLOADING;
+ },
+
+ /**
+ * An array of download listeners to notify when we receive
+ * nsIRequestObserver or nsIProgressEventSink method calls.
+ */
+ _listeners: [],
+
+ /**
+ * Adds a listener to the download process
+ * @param listener
+ * A download listener, implementing nsIRequestObserver and
+ * nsIProgressEventSink
+ */
+ addDownloadListener: function Downloader_addDownloadListener(listener) {
+ for (var i = 0; i < this._listeners.length; ++i) {
+ if (this._listeners[i] == listener)
+ return;
+ }
+ this._listeners.push(listener);
+ },
+
+ /**
+ * Removes a download listener
+ * @param listener
+ * The listener to remove.
+ */
+ removeDownloadListener: function Downloader_removeDownloadListener(listener) {
+ for (var i = 0; i < this._listeners.length; ++i) {
+ if (this._listeners[i] == listener) {
+ this._listeners.splice(i, 1);
+ return;
+ }
+ }
+ },
+
+ /**
+ * When the async request begins
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ */
+ onStartRequest: function Downloader_onStartRequest(request, context) {
+ if (request instanceof Ci.nsIIncrementalDownload)
+ LOG("Downloader:onStartRequest - original URI spec: " + request.URI.spec +
+ ", final URI spec: " + request.finalURI.spec);
+ // Always set finalURL in onStartRequest since it can change.
+ this._patch.finalURL = request.finalURI.spec;
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ um.saveUpdates();
+
+ var listeners = this._listeners.concat();
+ var listenerCount = listeners.length;
+ for (var i = 0; i < listenerCount; ++i)
+ listeners[i].onStartRequest(request, context);
+ },
+
+ /**
+ * When new data has been downloaded
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param progress
+ * The current number of bytes transferred
+ * @param maxProgress
+ * The total number of bytes that must be transferred
+ */
+ onProgress: function Downloader_onProgress(request, context, progress,
+ maxProgress) {
+ LOG("Downloader:onProgress - progress: " + progress + "/" + maxProgress);
+
+ if (progress > this._patch.size) {
+ LOG("Downloader:onProgress - progress: " + progress +
+ " is higher than patch size: " + this._patch.size);
+ // It's important that we use a different code than
+ // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference
+ // between a hash error and a wrong download error.
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_PATCH_SIZE_LARGER);
+ this.cancel(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ if (maxProgress != this._patch.size) {
+ LOG("Downloader:onProgress - maxProgress: " + maxProgress +
+ " is not equal to expected patch size: " + this._patch.size);
+ // It's important that we use a different code than
+ // NS_ERROR_CORRUPTED_CONTENT so that tests can verify the difference
+ // between a hash error and a wrong download error.
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_ERR_PATCH_SIZE_NOT_EQUAL);
+ this.cancel(Cr.NS_ERROR_UNEXPECTED);
+ return;
+ }
+
+ var listeners = this._listeners.concat();
+ var listenerCount = listeners.length;
+ for (var i = 0; i < listenerCount; ++i) {
+ var listener = listeners[i];
+ if (listener instanceof Ci.nsIProgressEventSink)
+ listener.onProgress(request, context, progress, maxProgress);
+ }
+ this.updateService._consecutiveSocketErrors = 0;
+ },
+
+ /**
+ * When we have new status text
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param status
+ * A status code
+ * @param statusText
+ * Human readable version of |status|
+ */
+ onStatus: function Downloader_onStatus(request, context, status, statusText) {
+ LOG("Downloader:onStatus - status: " + status + ", statusText: " +
+ statusText);
+
+ var listeners = this._listeners.concat();
+ var listenerCount = listeners.length;
+ for (var i = 0; i < listenerCount; ++i) {
+ var listener = listeners[i];
+ if (listener instanceof Ci.nsIProgressEventSink)
+ listener.onStatus(request, context, status, statusText);
+ }
+ },
+
+ /**
+ * When data transfer ceases
+ * @param request
+ * The nsIRequest object for the transfer
+ * @param context
+ * Additional data
+ * @param status
+ * Status code containing the reason for the cessation.
+ */
+ onStopRequest: function Downloader_onStopRequest(request, context, status) {
+ if (request instanceof Ci.nsIIncrementalDownload)
+ LOG("Downloader:onStopRequest - original URI spec: " + request.URI.spec +
+ ", final URI spec: " + request.finalURI.spec + ", status: " + status);
+
+ // XXX ehsan shouldShowPrompt should always be false here.
+ // But what happens when there is already a UI showing?
+ var state = this._patch.state;
+ var shouldShowPrompt = false;
+ var shouldRegisterOnlineObserver = false;
+ var shouldRetrySoon = false;
+ var deleteActiveUpdate = false;
+ var retryTimeout = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_RETRYTIMEOUT,
+ DEFAULT_SOCKET_RETRYTIMEOUT);
+ // Prevent the preference from setting a value greater than 10000.
+ retryTimeout = Math.min(retryTimeout, 10000);
+ var maxFail = getPref("getIntPref", PREF_APP_UPDATE_SOCKET_MAXERRORS,
+ DEFAULT_SOCKET_MAX_ERRORS);
+ // Prevent the preference from setting a value greater than 20.
+ maxFail = Math.min(maxFail, 20);
+ LOG("Downloader:onStopRequest - status: " + status + ", " +
+ "current fail: " + this.updateService._consecutiveSocketErrors + ", " +
+ "max fail: " + maxFail + ", " + "retryTimeout: " + retryTimeout);
+ if (Components.isSuccessCode(status)) {
+ if (this._verifyDownload()) {
+ if (shouldUseService()) {
+ state = STATE_PENDING_SERVICE;
+ } else if (getElevationRequired()) {
+ state = STATE_PENDING_ELEVATE;
+ } else {
+ state = STATE_PENDING;
+ }
+ if (this.background) {
+ shouldShowPrompt = !getCanStageUpdates();
+ }
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, AUSTLMY.DWNLD_SUCCESS);
+
+ // Tell the updater.exe we're ready to apply.
+ writeStatusFile(getUpdatesDir(), state);
+ writeVersionFile(getUpdatesDir(), this._update.appVersion);
+ this._update.installDate = (new Date()).getTime();
+ this._update.statusText = gUpdateBundle.GetStringFromName("installPending");
+ }
+ else {
+ LOG("Downloader:onStopRequest - download verification failed");
+ state = STATE_DOWNLOAD_FAILED;
+ status = Cr.NS_ERROR_CORRUPTED_CONTENT;
+
+ // Yes, this code is a string.
+ const vfCode = "verification_failed";
+ var message = getStatusTextFromCode(vfCode, vfCode);
+ this._update.statusText = message;
+
+ if (this._update.isCompleteUpdate || this._update.patchCount != 2)
+ deleteActiveUpdate = true;
+
+ // Destroy the updates directory, since we're done with it.
+ cleanUpUpdatesDir();
+ }
+ } else if (status == Cr.NS_ERROR_OFFLINE) {
+ // Register an online observer to try again.
+ // The online observer will continue the incremental download by
+ // calling downloadUpdate on the active update which continues
+ // downloading the file from where it was.
+ LOG("Downloader:onStopRequest - offline, register online observer: true");
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate,
+ AUSTLMY.DWNLD_RETRY_OFFLINE);
+ shouldRegisterOnlineObserver = true;
+ deleteActiveUpdate = false;
+ // Each of NS_ERROR_NET_TIMEOUT, ERROR_CONNECTION_REFUSED,
+ // NS_ERROR_NET_RESET and NS_ERROR_DOCUMENT_NOT_CACHED can be returned
+ // when disconnecting the internet while a download of a MAR is in
+ // progress. There may be others but I have not encountered them during
+ // testing.
+ } else if ((status == Cr.NS_ERROR_NET_TIMEOUT ||
+ status == Cr.NS_ERROR_CONNECTION_REFUSED ||
+ status == Cr.NS_ERROR_NET_RESET ||
+ status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) &&
+ this.updateService._consecutiveSocketErrors < maxFail) {
+ LOG("Downloader:onStopRequest - socket error, shouldRetrySoon: true");
+ let dwnldCode = AUSTLMY.DWNLD_RETRY_CONNECTION_REFUSED;
+ if (status == Cr.NS_ERROR_NET_TIMEOUT) {
+ dwnldCode = AUSTLMY.DWNLD_RETRY_NET_TIMEOUT;
+ } else if (status == Cr.NS_ERROR_NET_RESET) {
+ dwnldCode = AUSTLMY.DWNLD_RETRY_NET_RESET;
+ } else if (status == Cr.NS_ERROR_DOCUMENT_NOT_CACHED) {
+ dwnldCode = AUSTLMY.DWNLD_ERR_DOCUMENT_NOT_CACHED;
+ }
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);
+ shouldRetrySoon = true;
+ deleteActiveUpdate = false;
+ } else if (status != Cr.NS_BINDING_ABORTED &&
+ status != Cr.NS_ERROR_ABORT) {
+ LOG("Downloader:onStopRequest - non-verification failure");
+ let dwnldCode = AUSTLMY.DWNLD_ERR_BINDING_ABORTED;
+ if (status == Cr.NS_ERROR_ABORT) {
+ dwnldCode = AUSTLMY.DWNLD_ERR_ABORT;
+ }
+ AUSTLMY.pingDownloadCode(this.isCompleteUpdate, dwnldCode);
+
+ // Some sort of other failure, log this in the |statusText| property
+ state = STATE_DOWNLOAD_FAILED;
+
+ // XXXben - if |request| (The Incremental Download) provided a means
+ // for accessing the http channel we could do more here.
+
+ this._update.statusText = getStatusTextFromCode(status,
+ Cr.NS_BINDING_FAILED);
+
+ if (AppConstants.platform == "gonk") {
+ // bug891009: On FirefoxOS, manaully retry OTA download will reuse
+ // the Update object. We need to remove selected patch so that download
+ // can be triggered again successfully.
+ this._update.selectedPatch.selected = false;
+ }
+
+ // Destroy the updates directory, since we're done with it.
+ cleanUpUpdatesDir();
+
+ deleteActiveUpdate = true;
+ }
+ LOG("Downloader:onStopRequest - setting state to: " + state);
+ this._patch.state = state;
+ var um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ if (deleteActiveUpdate) {
+ this._update.installDate = (new Date()).getTime();
+ um.activeUpdate = null;
+ }
+ else if (um.activeUpdate) {
+ um.activeUpdate.state = state;
+ }
+ um.saveUpdates();
+
+ // Only notify listeners about the stopped state if we
+ // aren't handling an internal retry.
+ if (!shouldRetrySoon && !shouldRegisterOnlineObserver) {
+ var listeners = this._listeners.concat();
+ var listenerCount = listeners.length;
+ for (var i = 0; i < listenerCount; ++i) {
+ listeners[i].onStopRequest(request, context, status);
+ }
+ }
+
+ this._request = null;
+
+ if (state == STATE_DOWNLOAD_FAILED) {
+ var allFailed = true;
+ // Check if there is a complete update patch that can be downloaded.
+ if (!this._update.isCompleteUpdate && this._update.patchCount == 2) {
+ LOG("Downloader:onStopRequest - verification of patch failed, " +
+ "downloading complete update patch");
+ this._update.isCompleteUpdate = true;
+ let updateStatus = this.downloadUpdate(this._update);
+
+ if (updateStatus == STATE_NONE) {
+ cleanupActiveUpdate();
+ } else {
+ allFailed = false;
+ }
+ }
+
+ if (allFailed) {
+ LOG("Downloader:onStopRequest - all update patch downloads failed");
+ // If the update UI is not open (e.g. the user closed the window while
+ // downloading) and if at any point this was a foreground download
+ // notify the user about the error. If the update was a background
+ // update there is no notification since the user won't be expecting it.
+ if (!Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME)) {
+ this._update.QueryInterface(Ci.nsIWritablePropertyBag);
+ if (this._update.getProperty("foregroundDownload") == "true") {
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(this._update);
+ }
+ }
+
+ if (AppConstants.platform == "gonk") {
+ // We always forward errors in B2G, since Gaia controls the update UI
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(this._update);
+ }
+
+ // Prevent leaking the update object (bug 454964).
+ this._update = null;
+ }
+ // A complete download has been initiated or the failure was handled.
+ return;
+ }
+
+ if (state == STATE_PENDING || state == STATE_PENDING_SERVICE ||
+ state == STATE_PENDING_ELEVATE) {
+ if (getCanStageUpdates()) {
+ LOG("Downloader:onStopRequest - attempting to stage update: " +
+ this._update.name);
+
+ // Initiate the update in the background
+ try {
+ Cc["@mozilla.org/updates/update-processor;1"].
+ createInstance(Ci.nsIUpdateProcessor).
+ processUpdate(this._update);
+ } catch (e) {
+ // Fail gracefully in case the application does not support the update
+ // processor service.
+ LOG("Downloader:onStopRequest - failed to stage update. Exception: " +
+ e);
+ if (this.background) {
+ shouldShowPrompt = true;
+ }
+ }
+ }
+ }
+
+ // Do this after *everything* else, since it will likely cause the app
+ // to shut down.
+ if (shouldShowPrompt) {
+ // Notify the user that an update has been downloaded and is ready for
+ // installation (i.e. that they should restart the application). We do
+ // not notify on failed update attempts.
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateDownloaded(this._update, true);
+ }
+
+ if (shouldRegisterOnlineObserver) {
+ LOG("Downloader:onStopRequest - Registering online observer");
+ this.updateService._registerOnlineObserver();
+ } else if (shouldRetrySoon) {
+ LOG("Downloader:onStopRequest - Retrying soon");
+ this.updateService._consecutiveSocketErrors++;
+ if (this.updateService._retryTimer) {
+ this.updateService._retryTimer.cancel();
+ }
+ this.updateService._retryTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ this.updateService._retryTimer.initWithCallback(function() {
+ this._attemptResume();
+ }.bind(this.updateService), retryTimeout, Ci.nsITimer.TYPE_ONE_SHOT);
+ } else {
+ // Prevent leaking the update object (bug 454964)
+ this._update = null;
+ }
+ },
+
+ /**
+ * See nsIInterfaceRequestor.idl
+ */
+ getInterface: function Downloader_getInterface(iid) {
+ // The network request may require proxy authentication, so provide the
+ // default nsIAuthPrompt if requested.
+ if (iid.equals(Ci.nsIAuthPrompt)) {
+ var prompt = Cc["@mozilla.org/network/default-auth-prompt;1"].
+ createInstance();
+ return prompt.QueryInterface(iid);
+ }
+ throw Cr.NS_NOINTERFACE;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+ Ci.nsIProgressEventSink,
+ Ci.nsIInterfaceRequestor])
+};
+
+/**
+ * UpdatePrompt
+ * An object which can prompt the user with information about updates, request
+ * action, etc. Embedding clients can override this component with one that
+ * invokes a native front end.
+ * @constructor
+ */
+function UpdatePrompt() {
+}
+UpdatePrompt.prototype = {
+ /**
+ * See nsIUpdateService.idl
+ */
+ checkForUpdates: function UP_checkForUpdates() {
+ if (this._getAltUpdateWindow())
+ return;
+
+ this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME,
+ null, null);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ showUpdateAvailable: function UP_showUpdateAvailable(update) {
+ if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ this._getUpdateWindow() || this._getAltUpdateWindow()) {
+ return;
+ }
+
+ this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
+ UPDATE_WINDOW_NAME, "updatesavailable", update);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ showUpdateDownloaded: function UP_showUpdateDownloaded(update, background) {
+ if (background && getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false)) {
+ return;
+ }
+ // Trigger the display of the hamburger menu badge.
+ Services.obs.notifyObservers(null, "update-downloaded", update.state);
+
+ if (this._getAltUpdateWindow())
+ return;
+
+ if (background) {
+ this._showUnobtrusiveUI(null, URI_UPDATE_PROMPT_DIALOG, null,
+ UPDATE_WINDOW_NAME, "finishedBackground", update);
+ } else {
+ this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null,
+ UPDATE_WINDOW_NAME, "finishedBackground", update);
+ }
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ showUpdateError: function UP_showUpdateError(update) {
+ if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ this._getAltUpdateWindow())
+ return;
+
+ // In some cases, we want to just show a simple alert dialog.
+ // Replace with Array.prototype.includes when it has stabilized.
+ if (update.state == STATE_FAILED &&
+ (WRITE_ERRORS.indexOf(update.errorCode) != -1 ||
+ update.errorCode == FILESYSTEM_MOUNT_READWRITE_ERROR ||
+ update.errorCode == FOTA_GENERAL_ERROR ||
+ update.errorCode == FOTA_FILE_OPERATION_ERROR ||
+ update.errorCode == FOTA_RECOVERY_ERROR ||
+ update.errorCode == FOTA_UNKNOWN_ERROR)) {
+ var title = gUpdateBundle.GetStringFromName("updaterIOErrorTitle");
+ var text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg",
+ [Services.appinfo.name,
+ Services.appinfo.name], 2);
+ Services.ww.getNewPrompter(null).alert(title, text);
+ return;
+ }
+
+ if (update.errorCode == BACKGROUNDCHECK_MULTIPLE_FAILURES) {
+ this._showUIWhenIdle(null, URI_UPDATE_PROMPT_DIALOG, null,
+ UPDATE_WINDOW_NAME, null, update);
+ return;
+ }
+
+ this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null, UPDATE_WINDOW_NAME,
+ "errors", update);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ showUpdateHistory: function UP_showUpdateHistory(parent) {
+ this._showUI(parent, URI_UPDATE_HISTORY_DIALOG, "modal,dialog=yes",
+ "Update:History", null, null);
+ },
+
+ /**
+ * See nsIUpdateService.idl
+ */
+ showUpdateElevationRequired: function UP_showUpdateElevationRequired() {
+ if (getPref("getBoolPref", PREF_APP_UPDATE_SILENT, false) ||
+ this._getAltUpdateWindow()) {
+ return;
+ }
+
+ let um = Cc["@mozilla.org/updates/update-manager;1"].
+ getService(Ci.nsIUpdateManager);
+ this._showUI(null, URI_UPDATE_PROMPT_DIALOG, null,
+ UPDATE_WINDOW_NAME, "finishedBackground", um.activeUpdate);
+ },
+
+ /**
+ * Returns the update window if present.
+ */
+ _getUpdateWindow: function UP__getUpdateWindow() {
+ return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME);
+ },
+
+ /**
+ * Returns an alternative update window if present. When a window with this
+ * windowtype is open the application update service won't open the normal
+ * application update user interface window.
+ */
+ _getAltUpdateWindow: function UP__getAltUpdateWindow() {
+ let windowType = getPref("getCharPref", PREF_APP_UPDATE_ALTWINDOWTYPE, null);
+ if (!windowType)
+ return null;
+ return Services.wm.getMostRecentWindow(windowType);
+ },
+
+ /**
+ * Display the update UI after the prompt wait time has elapsed.
+ * @param parent
+ * A parent window, can be null
+ * @param uri
+ * The URI string of the dialog to show
+ * @param name
+ * The Window Name of the dialog to show, in case it is already open
+ * and can merely be focused
+ * @param page
+ * The page of the wizard to be displayed, if one is already open.
+ * @param update
+ * An update to pass to the UI in the window arguments.
+ * Can be null
+ */
+ _showUnobtrusiveUI: function UP__showUnobUI(parent, uri, features, name, page,
+ update) {
+ var observer = {
+ updatePrompt: this,
+ service: null,
+ timer: null,
+ notify: function () {
+ // the user hasn't restarted yet => prompt when idle
+ this.service.removeObserver(this, "quit-application");
+ // If the update window is already open skip showing the UI
+ if (this.updatePrompt._getUpdateWindow())
+ return;
+ this.updatePrompt._showUIWhenIdle(parent, uri, features, name, page, update);
+ },
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "quit-application":
+ if (this.timer)
+ this.timer.cancel();
+ this.service.removeObserver(this, "quit-application");
+ break;
+ }
+ }
+ };
+
+ // bug 534090 - show the UI for update available notifications when the
+ // the system has been idle for at least IDLE_TIME.
+ if (page == "updatesavailable") {
+ var idleService = Cc["@mozilla.org/widget/idleservice;1"].
+ getService(Ci.nsIIdleService);
+ // Don't allow the preference to set a value greater than 600 seconds for the idle time.
+ const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600);
+ if (idleService.idleTime / 1000 >= IDLE_TIME) {
+ this._showUI(parent, uri, features, name, page, update);
+ return;
+ }
+ }
+
+ observer.service = Services.obs;
+ observer.service.addObserver(observer, "quit-application", false);
+
+ // bug 534090 - show the UI when idle for update available notifications.
+ if (page == "updatesavailable") {
+ this._showUIWhenIdle(parent, uri, features, name, page, update);
+ return;
+ }
+
+ // Give the user x seconds to react before prompting as defined by
+ // promptWaitTime
+ observer.timer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ observer.timer.initWithCallback(observer, update.promptWaitTime * 1000,
+ observer.timer.TYPE_ONE_SHOT);
+ },
+
+ /**
+ * Show the UI when the user was idle
+ * @param parent
+ * A parent window, can be null
+ * @param uri
+ * The URI string of the dialog to show
+ * @param name
+ * The Window Name of the dialog to show, in case it is already open
+ * and can merely be focused
+ * @param page
+ * The page of the wizard to be displayed, if one is already open.
+ * @param update
+ * An update to pass to the UI in the window arguments.
+ * Can be null
+ */
+ _showUIWhenIdle: function UP__showUIWhenIdle(parent, uri, features, name,
+ page, update) {
+ var idleService = Cc["@mozilla.org/widget/idleservice;1"].
+ getService(Ci.nsIIdleService);
+
+ // Don't allow the preference to set a value greater than 600 seconds for the idle time.
+ const IDLE_TIME = Math.min(getPref("getIntPref", PREF_APP_UPDATE_IDLETIME, 60), 600);
+ if (idleService.idleTime / 1000 >= IDLE_TIME) {
+ this._showUI(parent, uri, features, name, page, update);
+ } else {
+ var observer = {
+ updatePrompt: this,
+ observe: function (aSubject, aTopic, aData) {
+ switch (aTopic) {
+ case "idle":
+ // If the update window is already open skip showing the UI
+ if (!this.updatePrompt._getUpdateWindow())
+ this.updatePrompt._showUI(parent, uri, features, name, page, update);
+ // fall thru
+ case "quit-application":
+ idleService.removeIdleObserver(this, IDLE_TIME);
+ Services.obs.removeObserver(this, "quit-application");
+ break;
+ }
+ }
+ };
+ idleService.addIdleObserver(observer, IDLE_TIME);
+ Services.obs.addObserver(observer, "quit-application", false);
+ }
+ },
+
+ /**
+ * Show the Update Checking UI
+ * @param parent
+ * A parent window, can be null
+ * @param uri
+ * The URI string of the dialog to show
+ * @param name
+ * The Window Name of the dialog to show, in case it is already open
+ * and can merely be focused
+ * @param page
+ * The page of the wizard to be displayed, if one is already open.
+ * @param update
+ * An update to pass to the UI in the window arguments.
+ * Can be null
+ */
+ _showUI: function UP__showUI(parent, uri, features, name, page, update) {
+ var ary = null;
+ if (update) {
+ ary = Cc["@mozilla.org/array;1"].
+ createInstance(Ci.nsIMutableArray);
+ ary.appendElement(update, /* weak =*/ false);
+ }
+
+ var win = this._getUpdateWindow();
+ if (win) {
+ if (page && "setCurrentPage" in win)
+ win.setCurrentPage(page);
+ win.focus();
+ }
+ else {
+ var openFeatures = "chrome,centerscreen,dialog=no,resizable=no,titlebar,toolbar=no";
+ if (features)
+ openFeatures += "," + features;
+ Services.ww.openWindow(parent, uri, "", openFeatures, ary);
+ }
+ },
+
+ classDescription: "Update Prompt",
+ contractID: "@mozilla.org/updates/update-prompt;1",
+ classID: Components.ID("{27ABA825-35B5-4018-9FDD-F99250A0E722}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdatePrompt])
+};
+
+var components = [UpdateService, Checker, UpdatePrompt, UpdateManager];
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
diff --git a/toolkit/mozapps/update/nsUpdateService.manifest b/toolkit/mozapps/update/nsUpdateService.manifest
new file mode 100644
index 000000000..59dbf0b5a
--- /dev/null
+++ b/toolkit/mozapps/update/nsUpdateService.manifest
@@ -0,0 +1,12 @@
+component {B3C290A6-3943-4B89-8BBE-C01EB7B3B311} nsUpdateService.js
+contract @mozilla.org/updates/update-service;1 {B3C290A6-3943-4B89-8BBE-C01EB7B3B311}
+category update-timer nsUpdateService @mozilla.org/updates/update-service;1,getService,background-update-timer,app.update.interval,43200,86400
+component {093C2356-4843-4C65-8709-D7DBCBBE7DFB} nsUpdateService.js
+contract @mozilla.org/updates/update-manager;1 {093C2356-4843-4C65-8709-D7DBCBBE7DFB}
+component {898CDC9B-E43F-422F-9CC4-2F6291B415A3} nsUpdateService.js
+contract @mozilla.org/updates/update-checker;1 {898CDC9B-E43F-422F-9CC4-2F6291B415A3}
+component {27ABA825-35B5-4018-9FDD-F99250A0E722} nsUpdateService.js
+contract @mozilla.org/updates/update-prompt;1 {27ABA825-35B5-4018-9FDD-F99250A0E722}
+component {e43b0010-04ba-4da6-b523-1f92580bc150} nsUpdateServiceStub.js
+contract @mozilla.org/updates/update-service-stub;1 {e43b0010-04ba-4da6-b523-1f92580bc150}
+category profile-after-change nsUpdateServiceStub @mozilla.org/updates/update-service-stub;1
diff --git a/toolkit/mozapps/update/nsUpdateServiceStub.js b/toolkit/mozapps/update/nsUpdateServiceStub.js
new file mode 100644
index 000000000..dbb841b84
--- /dev/null
+++ b/toolkit/mozapps/update/nsUpdateServiceStub.js
@@ -0,0 +1,45 @@
+/* -*- 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/. */
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm", this);
+Cu.import("resource://gre/modules/FileUtils.jsm", this);
+
+const DIR_UPDATES = "updates";
+const FILE_UPDATE_STATUS = "update.status";
+
+const KEY_UPDROOT = "UpdRootD";
+
+/**
+ * Gets the specified directory at the specified hierarchy under the update root
+ * directory without creating it if it doesn't exist.
+ * @param pathArray
+ * An array of path components to locate beneath the directory
+ * specified by |key|
+ * @return nsIFile object for the location specified.
+ */
+function getUpdateDirNoCreate(pathArray) {
+ return FileUtils.getDir(KEY_UPDROOT, pathArray, false);
+}
+
+function UpdateServiceStub() {
+ let statusFile = getUpdateDirNoCreate([DIR_UPDATES, "0"]);
+ statusFile.append(FILE_UPDATE_STATUS);
+ // If the update.status file exists then initiate post update processing.
+ if (statusFile.exists()) {
+ let aus = Cc["@mozilla.org/updates/update-service;1"].
+ getService(Ci.nsIApplicationUpdateService).
+ QueryInterface(Ci.nsIObserver);
+ aus.observe(null, "post-update-processing", "");
+ }
+}
+UpdateServiceStub.prototype = {
+ observe: function() {},
+ classID: Components.ID("{e43b0010-04ba-4da6-b523-1f92580bc150}"),
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+
+this.NSGetFactory = XPCOMUtils.generateNSGetFactory([UpdateServiceStub]);
diff --git a/toolkit/mozapps/update/tests/Makefile.in b/toolkit/mozapps/update/tests/Makefile.in
new file mode 100644
index 000000000..0b8d19aa2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/Makefile.in
@@ -0,0 +1,39 @@
+# 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/.
+
+XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/$(relativesrcdir)
+
+pp_const_file = $(srcdir)/data/xpcshellConstantsPP.js
+
+PP_TARGETS += aus-test-const
+aus-test-const := $(pp_const_file)
+aus-test-const_PATH := $(XPCSHELLTESTROOT)/data
+aus-test-const_FLAGS := -Fsubstitution $(DEFINES) $(ACDEFINES)
+aus-test-const_TARGET := misc
+
+INI_TEST_FILES = \
+ TestAUSReadStrings1.ini \
+ TestAUSReadStrings2.ini \
+ TestAUSReadStrings3.ini \
+ $(NULL)
+
+MOZ_WINCONSOLE = 1
+
+include $(topsrcdir)/config/rules.mk
+
+# TestAUSReadStrings runs during check in the following directory with a Unicode
+# char in order to test bug 473417 on Windows.
+ifeq ($(OS_ARCH),WINNT)
+bug473417dir = test_bug473417-ó
+else
+bug473417dir = test_bug473417
+endif
+
+check::
+ $(RM) -rf $(DEPTH)/_tests/updater/ && $(NSINSTALL) -D $(DEPTH)/_tests/updater/$(bug473417dir)/
+ for i in $(INI_TEST_FILES); do \
+ $(INSTALL) $(srcdir)/$$i $(DEPTH)/_tests/updater/$(bug473417dir)/; \
+ done
+ $(INSTALL) $(FINAL_TARGET)/TestAUSReadStrings$(BIN_SUFFIX) $(DEPTH)/_tests/updater/$(bug473417dir)/
+ @$(RUN_TEST_PROGRAM) $(DEPTH)/_tests/updater/$(bug473417dir)/TestAUSReadStrings$(BIN_SUFFIX)
diff --git a/toolkit/mozapps/update/tests/TestAUSHelper.cpp b/toolkit/mozapps/update/tests/TestAUSHelper.cpp
new file mode 100644
index 000000000..f71103b7a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/TestAUSHelper.cpp
@@ -0,0 +1,423 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+#ifdef XP_WIN
+# include <windows.h>
+# include <wintrust.h>
+# include <tlhelp32.h>
+# include <softpub.h>
+# include <direct.h>
+# include <io.h>
+ typedef WCHAR NS_tchar;
+# define NS_main wmain
+# ifndef F_OK
+# define F_OK 00
+# endif
+# ifndef W_OK
+# define W_OK 02
+# endif
+# ifndef R_OK
+# define R_OK 04
+# endif
+# if defined(_MSC_VER) && _MSC_VER < 1900
+# define stat _stat
+# endif
+# define NS_T(str) L ## str
+# define NS_tsnprintf(dest, count, fmt, ...) \
+ { \
+ int _count = count - 1; \
+ _snwprintf(dest, _count, fmt, ##__VA_ARGS__); \
+ dest[_count] = L'\0'; \
+ }
+# define NS_taccess _waccess
+# define NS_tchdir _wchdir
+# define NS_tfopen _wfopen
+# define NS_tstrcmp wcscmp
+# define NS_ttoi _wtoi
+# define NS_tstat _wstat
+# define NS_tgetcwd _wgetcwd
+# define LOG_S "%S"
+
+#include "../common/updatehelper.h"
+#include "../common/certificatecheck.h"
+
+#else
+# include <unistd.h>
+# define NS_main main
+ typedef char NS_tchar;
+# define NS_T(str) str
+# define NS_tsnprintf snprintf
+# define NS_taccess access
+# define NS_tchdir chdir
+# define NS_tfopen fopen
+# define NS_tstrcmp strcmp
+# define NS_ttoi atoi
+# define NS_tstat stat
+# define NS_tgetcwd getcwd
+# define NS_tfputs fputs
+# define LOG_S "%s"
+#endif
+
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+
+#ifndef MAXPATHLEN
+# ifdef PATH_MAX
+# define MAXPATHLEN PATH_MAX
+# elif defined(MAX_PATH)
+# define MAXPATHLEN MAX_PATH
+# elif defined(_MAX_PATH)
+# define MAXPATHLEN _MAX_PATH
+# elif defined(CCHMAXPATH)
+# define MAXPATHLEN CCHMAXPATH
+# else
+# define MAXPATHLEN 1024
+# endif
+#endif
+
+static void
+WriteMsg(const NS_tchar *path, const char *status)
+{
+ FILE* outFP = NS_tfopen(path, NS_T("wb"));
+ if (!outFP) {
+ return;
+ }
+
+ fprintf(outFP, "%s\n", status);
+ fclose(outFP);
+ outFP = nullptr;
+}
+
+static bool
+CheckMsg(const NS_tchar *path, const char *expected)
+{
+ if (NS_taccess(path, F_OK)) {
+ return false;
+ }
+
+ FILE *inFP = NS_tfopen(path, NS_T("rb"));
+ if (!inFP) {
+ return false;
+ }
+
+ struct stat ms;
+ if (fstat(fileno(inFP), &ms)) {
+ fclose(inFP);
+ inFP = nullptr;
+ return false;
+ }
+
+ char *mbuf = (char *) malloc(ms.st_size + 1);
+ if (!mbuf) {
+ fclose(inFP);
+ inFP = nullptr;
+ return false;
+ }
+
+ size_t r = ms.st_size;
+ char *rb = mbuf;
+ size_t c = fread(rb, sizeof(char), 50, inFP);
+ r -= c;
+ rb += c;
+ if (c == 0 && r) {
+ free(mbuf);
+ fclose(inFP);
+ inFP = nullptr;
+ return false;
+ }
+ mbuf[ms.st_size] = '\0';
+ rb = mbuf;
+
+ bool isMatch = strcmp(rb, expected) == 0;
+ free(mbuf);
+ fclose(inFP);
+ inFP = nullptr;
+ return isMatch;
+}
+
+int NS_main(int argc, NS_tchar **argv)
+{
+ if (argc == 2) {
+ if (!NS_tstrcmp(argv[1], NS_T("post-update-async")) ||
+ !NS_tstrcmp(argv[1], NS_T("post-update-sync"))) {
+ NS_tchar exePath[MAXPATHLEN];
+#ifdef XP_WIN
+ if (!::GetModuleFileNameW(0, exePath, MAXPATHLEN)) {
+ return 1;
+ }
+#else
+ strcpy(exePath, argv[0]);
+#endif
+ NS_tchar runFilePath[MAXPATHLEN];
+ NS_tsnprintf(runFilePath, sizeof(runFilePath)/sizeof(runFilePath[0]),
+ NS_T("%s.running"), exePath);
+#ifdef XP_WIN
+ if (!NS_taccess(runFilePath, F_OK)) {
+ // This makes it possible to check if the post update process was
+ // launched twice which happens when the service performs an update.
+ NS_tchar runFilePathBak[MAXPATHLEN];
+ NS_tsnprintf(runFilePathBak, sizeof(runFilePathBak)/sizeof(runFilePathBak[0]),
+ NS_T("%s.bak"), runFilePath);
+ MoveFileExW(runFilePath, runFilePathBak, MOVEFILE_REPLACE_EXISTING);
+ }
+#endif
+ WriteMsg(runFilePath, "running");
+
+ if (!NS_tstrcmp(argv[1], NS_T("post-update-sync"))) {
+#ifdef XP_WIN
+ Sleep(2000);
+#else
+ sleep(2);
+#endif
+ }
+
+ NS_tchar logFilePath[MAXPATHLEN];
+ NS_tsnprintf(logFilePath, sizeof(logFilePath)/sizeof(logFilePath[0]),
+ NS_T("%s.log"), exePath);
+ WriteMsg(logFilePath, "post-update");
+ return 0;
+ }
+ }
+
+ if (argc < 3) {
+ fprintf(stderr, \
+ "\n" \
+ "Application Update Service Test Helper\n" \
+ "\n" \
+ "Usage: WORKINGDIR INFILE OUTFILE -s SECONDS [FILETOLOCK]\n" \
+ " or: WORKINGDIR LOGFILE [ARG2 ARG3...]\n" \
+ " or: signature-check filepath\n" \
+ " or: setup-symlink dir1 dir2 file symlink\n" \
+ " or: remove-symlink dir1 dir2 file symlink\n" \
+ " or: check-symlink symlink\n" \
+ " or: post-update\n" \
+ "\n" \
+ " WORKINGDIR \tThe relative path to the working directory to use.\n" \
+ " INFILE \tThe relative path from the working directory for the file to\n" \
+ " \tread actions to perform such as finish.\n" \
+ " OUTFILE \tThe relative path from the working directory for the file to\n" \
+ " \twrite status information.\n" \
+ " SECONDS \tThe number of seconds to sleep.\n" \
+ " FILETOLOCK \tThe relative path from the working directory to an existing\n" \
+ " \tfile to open exlusively.\n" \
+ " \tOnly available on Windows platforms and silently ignored on\n" \
+ " \tother platforms.\n" \
+ " LOGFILE \tThe relative path from the working directory to log the\n" \
+ " \tcommand line arguments.\n" \
+ " ARG2 ARG3...\tArguments to write to the LOGFILE after the preceding command\n" \
+ " \tline arguments.\n" \
+ "\n" \
+ "Note: All paths must be relative.\n" \
+ "\n");
+ return 1;
+ }
+
+ 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
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("setup-symlink"))) {
+#ifdef XP_UNIX
+ NS_tchar path[MAXPATHLEN];
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s"), NS_T("/tmp"), argv[2]);
+ mkdir(path, 0755);
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3]);
+ mkdir(path, 0755);
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], argv[4]);
+ FILE * file = NS_tfopen(path, NS_T("w"));
+ if (file) {
+ NS_tfputs(NS_T("test"), file);
+ fclose(file);
+ }
+ if (symlink(path, argv[5]) != 0) {
+ return 1;
+ }
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s"), NS_T("/tmp"), argv[2]);
+ if (argc > 6 && !NS_tstrcmp(argv[6], NS_T("change-perm"))) {
+ chmod(path, 0644);
+ }
+ return 0;
+#else
+ // Not implemented on non-Unix platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("remove-symlink"))) {
+#ifdef XP_UNIX
+ NS_tchar path[MAXPATHLEN];
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s"), NS_T("/tmp"), argv[2]);
+ chmod(path, 0755);
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3], argv[4]);
+ unlink(path);
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s/%s"), NS_T("/tmp"), argv[2], argv[3]);
+ rmdir(path);
+ NS_tsnprintf(path, sizeof(path)/sizeof(path[0]),
+ NS_T("%s/%s"), NS_T("/tmp"), argv[2]);
+ rmdir(path);
+ return 0;
+#else
+ // Not implemented on non-Unix platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("check-symlink"))) {
+#ifdef XP_UNIX
+ struct stat ss;
+ lstat(argv[2], &ss);
+ return S_ISLNK(ss.st_mode) ? 0 : 1;
+#else
+ // Not implemented on non-Unix platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("wait-for-service-stop"))) {
+#ifdef XP_WIN
+ const int maxWaitSeconds = NS_ttoi(argv[3]);
+ LPCWSTR serviceName = argv[2];
+ DWORD serviceState = WaitForServiceStop(serviceName, maxWaitSeconds);
+ if (SERVICE_STOPPED == serviceState) {
+ return 0;
+ } else {
+ return serviceState;
+ }
+#else
+ // Not implemented on non-Windows platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("wait-for-application-exit"))) {
+#ifdef XP_WIN
+ const int maxWaitSeconds = NS_ttoi(argv[3]);
+ LPCWSTR application = argv[2];
+ DWORD ret = WaitForProcessExit(application, maxWaitSeconds);
+ if (ERROR_SUCCESS == ret) {
+ return 0;
+ } else if (WAIT_TIMEOUT == ret) {
+ return 1;
+ } else {
+ return 2;
+ }
+#else
+ // Not implemented on non-Windows platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("is-process-running"))) {
+#ifdef XP_WIN
+ LPCWSTR application = argv[2];
+ return (ERROR_NOT_FOUND == IsProcessRunning(application)) ? 0 : 1;
+#else
+ // Not implemented on non-Windows platforms
+ return 1;
+#endif
+ }
+
+ if (!NS_tstrcmp(argv[1], NS_T("launch-service"))) {
+#ifdef XP_WIN
+ DWORD ret = LaunchServiceSoftwareUpdateCommand(argc - 2, (LPCWSTR *)argv + 2);
+ if (ret != ERROR_SUCCESS) {
+ // 192 is used to avoid reusing a possible return value from the call to
+ // WaitForServiceStop
+ return 0x000000C0;
+ }
+ // Wait a maximum of 120 seconds.
+ DWORD lastState = WaitForServiceStop(SVC_NAME, 120);
+ if (SERVICE_STOPPED == lastState) {
+ return 0;
+ }
+ return lastState;
+#else
+ // Not implemented on non-Windows platforms
+ return 1;
+#endif
+ }
+
+ if (NS_tchdir(argv[1]) != 0) {
+ return 1;
+ }
+
+ // File in use test helper section
+ if (!NS_tstrcmp(argv[4], NS_T("-s"))) {
+ NS_tchar *cwd = NS_tgetcwd(nullptr, 0);
+ NS_tchar inFilePath[MAXPATHLEN];
+ NS_tsnprintf(inFilePath, sizeof(inFilePath)/sizeof(inFilePath[0]),
+ NS_T("%s/%s"), cwd, argv[2]);
+ NS_tchar outFilePath[MAXPATHLEN];
+ NS_tsnprintf(outFilePath, sizeof(outFilePath)/sizeof(outFilePath[0]),
+ NS_T("%s/%s"), cwd, argv[3]);
+
+ int seconds = NS_ttoi(argv[5]);
+#ifdef XP_WIN
+ HANDLE hFile = INVALID_HANDLE_VALUE;
+ if (argc == 7) {
+ hFile = CreateFileW(argv[6],
+ DELETE | GENERIC_WRITE, 0,
+ nullptr, OPEN_EXISTING, 0, nullptr);
+ if (hFile == INVALID_HANDLE_VALUE) {
+ WriteMsg(outFilePath, "error_locking");
+ return 1;
+ }
+ }
+
+ WriteMsg(outFilePath, "sleeping");
+ int i = 0;
+ while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) {
+ Sleep(1000);
+ }
+
+ if (argc == 7) {
+ CloseHandle(hFile);
+ }
+#else
+ WriteMsg(outFilePath, "sleeping");
+ int i = 0;
+ while (!CheckMsg(inFilePath, "finish\n") && i++ <= seconds) {
+ sleep(1);
+ }
+#endif
+ WriteMsg(outFilePath, "finished");
+ return 0;
+ }
+
+ {
+ // Command line argument test helper section
+ NS_tchar logFilePath[MAXPATHLEN];
+ NS_tsnprintf(logFilePath, sizeof(logFilePath)/sizeof(logFilePath[0]),
+ NS_T("%s"), argv[2]);
+
+ FILE* logFP = NS_tfopen(logFilePath, NS_T("wb"));
+ for (int i = 1; i < argc; ++i) {
+ fprintf(logFP, LOG_S "\n", argv[i]);
+ }
+
+ fclose(logFP);
+ logFP = nullptr;
+ }
+
+ return 0;
+}
diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp
new file mode 100644
index 000000000..c1de44f8e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/TestAUSReadStrings.cpp
@@ -0,0 +1,172 @@
+/* -*- Mode: C++; tab-width: 2; 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/. */
+
+/**
+ * This binary tests the updater's ReadStrings ini parser and should run in a
+ * directory with a Unicode character to test bug 473417.
+ */
+#ifdef XP_WIN
+ #include <windows.h>
+ #define NS_main wmain
+ #define NS_tstrrchr wcsrchr
+ #define NS_T(str) L ## str
+ #define PATH_SEPARATOR_CHAR L'\\'
+ // On Windows, argv[0] can also have forward slashes instead
+ #define ALT_PATH_SEPARATOR_CHAR L'/'
+#else
+ #include <unistd.h>
+ #define NS_main main
+ #define NS_tstrrchr strrchr
+ #define NS_T(str) str
+ #define PATH_SEPARATOR_CHAR '/'
+#endif
+
+#include <stdio.h>
+#include <stdarg.h>
+#include <string.h>
+
+#include "updater/resource.h"
+#include "updater/progressui.h"
+#include "common/readstrings.h"
+#include "common/errors.h"
+#include "mozilla/ArrayUtils.h"
+
+#ifndef MAXPATHLEN
+# ifdef PATH_MAX
+# define MAXPATHLEN PATH_MAX
+# elif defined(MAX_PATH)
+# define MAXPATHLEN MAX_PATH
+# elif defined(_MAX_PATH)
+# define MAXPATHLEN _MAX_PATH
+# elif defined(CCHMAXPATH)
+# define MAXPATHLEN CCHMAXPATH
+# else
+# define MAXPATHLEN 1024
+# endif
+#endif
+
+#define TEST_NAME "Updater ReadStrings"
+
+using namespace mozilla;
+
+static int gFailCount = 0;
+
+/**
+ * Prints the given failure message and arguments using printf, prepending
+ * "TEST-UNEXPECTED-FAIL " for the benefit of the test harness and
+ * appending "\n" to eliminate having to type it at each call site.
+ */
+void fail(const char* msg, ...)
+{
+ va_list ap;
+
+ printf("TEST-UNEXPECTED-FAIL | ");
+
+ va_start(ap, msg);
+ vprintf(msg, ap);
+ va_end(ap);
+
+ putchar('\n');
+ ++gFailCount;
+}
+
+int NS_main(int argc, NS_tchar **argv)
+{
+ printf("Running TestAUSReadStrings tests\n");
+
+ int rv = 0;
+ int retval;
+ NS_tchar inifile[MAXPATHLEN];
+ StringTable testStrings;
+
+ NS_tchar *slash = NS_tstrrchr(argv[0], PATH_SEPARATOR_CHAR);
+#ifdef ALT_PATH_SEPARATOR_CHAR
+ NS_tchar *altslash = NS_tstrrchr(argv[0], ALT_PATH_SEPARATOR_CHAR);
+ slash = (slash > altslash) ? slash : altslash;
+#endif // ALT_PATH_SEPARATOR_CHAR
+
+ if (!slash) {
+ fail("%s | unable to find platform specific path separator (check 1)", TEST_NAME);
+ return 20;
+ }
+
+ *(++slash) = '\0';
+ // Test success when the ini file exists with both Title and Info in the
+ // Strings section and the values for Title and Info.
+ NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings1.ini"), argv[0]);
+ retval = ReadStrings(inifile, &testStrings);
+ if (retval == OK) {
+ if (strcmp(testStrings.title, "Title Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" \
+ "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " \
+ "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " \
+ "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " \
+ "\xE6\xB8\xAC\xE8\xA9\xA6 " \
+ "\xE6\xB5\x8B\xE8\xAF\x95") != 0) {
+ rv = 21;
+ fail("%s | Title ini value incorrect (check 3)", TEST_NAME);
+ }
+
+ if (strcmp(testStrings.info, "Info Test - \xD0\x98\xD1\x81\xD0\xBF\xD1\x8B" \
+ "\xD1\x82\xD0\xB0\xD0\xBD\xD0\xB8\xD0\xB5 " \
+ "\xCE\x94\xCE\xBF\xCE\xBA\xCE\xB9\xCE\xBC\xCE\xAE " \
+ "\xE3\x83\x86\xE3\x82\xB9\xE3\x83\x88 " \
+ "\xE6\xB8\xAC\xE8\xA9\xA6 " \
+ "\xE6\xB5\x8B\xE8\xAF\x95\xE2\x80\xA6") != 0) {
+ rv = 22;
+ fail("%s | Info ini value incorrect (check 4)", TEST_NAME);
+ }
+ } else {
+ fail("%s | ReadStrings returned %i (check 2)", TEST_NAME, retval);
+ rv = 23;
+ }
+
+ // Test failure when the ini file exists without Title and with Info in the
+ // Strings section.
+ NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings2.ini"), argv[0]);
+ retval = ReadStrings(inifile, &testStrings);
+ if (retval != PARSE_ERROR) {
+ rv = 24;
+ fail("%s | ReadStrings returned %i (check 5)", TEST_NAME, retval);
+ }
+
+ // Test failure when the ini file exists with Title and without Info in the
+ // Strings section.
+ NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), argv[0]);
+ retval = ReadStrings(inifile, &testStrings);
+ if (retval != PARSE_ERROR) {
+ rv = 25;
+ fail("%s | ReadStrings returned %i (check 6)", TEST_NAME, retval);
+ }
+
+ // Test failure when the ini file doesn't exist
+ NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStringsBogus.ini"), argv[0]);
+ retval = ReadStrings(inifile, &testStrings);
+ if (retval != READ_ERROR) {
+ rv = 26;
+ fail("%s | ini file doesn't exist (check 7)", TEST_NAME);
+ }
+
+ // Test reading a non-default section name
+ NS_tsnprintf(inifile, ArrayLength(inifile), NS_T("%sTestAUSReadStrings3.ini"), argv[0]);
+ retval = ReadStrings(inifile, "Title\0", 1, &testStrings.title, "BogusSection2");
+ if (retval == OK) {
+ if (strcmp(testStrings.title, "Bogus Title") != 0) {
+ rv = 27;
+ fail("%s | Title ini value incorrect (check 9)", TEST_NAME);
+ }
+ } else {
+ fail("%s | ReadStrings returned %i (check 8)", TEST_NAME, retval);
+ rv = 28;
+ }
+
+
+ if (rv == 0) {
+ printf("TEST-PASS | %s | all checks passed\n", TEST_NAME);
+ } else {
+ fail("%s | %i out of 9 checks failed", TEST_NAME, gFailCount);
+ }
+
+ return rv;
+}
diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini
new file mode 100644
index 000000000..5ab13c185
--- /dev/null
+++ b/toolkit/mozapps/update/tests/TestAUSReadStrings1.ini
@@ -0,0 +1,47 @@
+; This file is in the UTF-8 encoding
+
+[BogusSection1]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
+
+[Strings]
+
+Bogus1=Bogus1
+
+; Comment
+
+Title=Title Test - ИÑпытание Δοκιμή テスト 測試 测试
+
+; Comment
+
+Bogus2=Bogus2
+
+; Comment
+
+Info=Info Test - ИÑпытание Δοκιμή テスト 測試 测试…
+
+; Comment
+
+Bogus3=Bogus3
+
+; Comment
+
+[BogusSection2]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini
new file mode 100644
index 000000000..8291a7c94
--- /dev/null
+++ b/toolkit/mozapps/update/tests/TestAUSReadStrings2.ini
@@ -0,0 +1,39 @@
+; This file is in the UTF-8 encoding
+
+[BogusSection1]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
+
+[Strings]
+
+Bogus1=Bogus1
+
+; Comment
+
+Info=Info
+
+; Comment
+
+Bogus2=Bogus2
+
+; Comment
+
+[BogusSection2]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
diff --git a/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini
new file mode 100644
index 000000000..a64d1232e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/TestAUSReadStrings3.ini
@@ -0,0 +1,39 @@
+; This file is in the UTF-8 encoding
+
+[BogusSection1]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
+
+[Strings]
+
+Bogus1=Bogus1
+
+; Comment
+
+Title=Title
+
+; Comment
+
+Bogus2=Bogus2
+
+; Comment
+
+[BogusSection2]
+
+; Comment
+
+Title=Bogus Title
+
+; Comment
+
+Info=Bogus Info
+
+; Comment
diff --git a/toolkit/mozapps/update/tests/chrome/.eslintrc.js b/toolkit/mozapps/update/tests/chrome/.eslintrc.js
new file mode 100644
index 000000000..8c0f4f574
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/mochitest/chrome.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/update/tests/chrome/chrome.ini b/toolkit/mozapps/update/tests/chrome/chrome.ini
new file mode 100644
index 000000000..88e3dd4e8
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/chrome.ini
@@ -0,0 +1,64 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+tags = appupdate
+support-files =
+ utils.js
+ update.sjs
+
+; mochitest-chrome tests must start with "test_" and are executed in sorted
+; order and not in the order specified in the manifest.
+[test_0010_background_basic.xul]
+[test_0011_check_basic.xul]
+[test_0012_check_basic_staging.xul]
+skip-if = asan
+reason = Bug 1168003
+[test_0013_check_no_updates.xul]
+[test_0014_check_error_xml_malformed.xul]
+[test_0061_check_verifyFailPartial_noComplete.xul]
+[test_0062_check_verifyFailComplete_noPartial.xul]
+[test_0063_check_verifyFailPartialComplete.xul]
+[test_0064_check_verifyFailPartial_successComplete.xul]
+[test_0071_notify_verifyFailPartial_noComplete.xul]
+[test_0072_notify_verifyFailComplete_noPartial.xul]
+[test_0073_notify_verifyFailPartialComplete.xul]
+[test_0074_notify_verifyFailPartial_successComplete.xul]
+[test_0081_error_patchApplyFailure_partial_only.xul]
+[test_0082_error_patchApplyFailure_complete_only.xul]
+[test_0083_error_patchApplyFailure_partial_complete.xul]
+[test_0084_error_patchApplyFailure_verify_failed.xul]
+[test_0085_error_patchApplyFailure_partial_complete_staging.xul]
+skip-if = asan
+reason = Bug 1168003
+[test_0092_finishedBackground.xul]
+[test_0093_restartNotification.xul]
+[test_0094_restartNotification_remote.xul]
+[test_0095_restartNotification_remoteInvalidNumber.xul]
+[test_0096_restartNotification_stagedBackground.xul]
+skip-if = asan
+reason = Bug 1168003
+[test_0097_restartNotification_stagedServiceBackground.xul]
+skip-if = os != 'win'
+reason = only Windows has the maintenance service.
+[test_0101_background_restartNotification.xul]
+[test_0102_background_restartNotification_staging.xul]
+skip-if = asan
+reason = Bug 1168003
+[test_0103_background_restartNotification_stagingService.xul]
+skip-if = os != 'win'
+reason = only Windows has the maintenance service.
+[test_0111_neverButton_basic.xul]
+[test_0113_showNeverForVersionRemovedWithPref.xul]
+[test_0151_notify_backgroundCheckError.xul]
+[test_0152_notify_backgroundCheckOfflineRetry.xul]
+[test_0161_check_unsupported.xul]
+[test_0162_notify_unsupported.xul]
+[test_0171_check_noPerms_manual.xul]
+skip-if = os != 'win'
+reason = test must be able to prevent file deletion.
+[test_0172_notify_noPerms_manual.xul]
+skip-if = os != 'win'
+reason = test must be able to prevent file deletion.
+[test_9999_cleanup.xul]
diff --git a/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul
new file mode 100644
index 000000000..8d088cc8a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0010_background_basic.xul
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: basic, download, and finished"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" +
+ getVersionParams();
+ setUpdateURL(url);
+
+ gAUS.checkForBackgroundUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul
new file mode 100644
index 000000000..12b5302a4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0011_check_basic.xul
@@ -0,0 +1,51 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download, and finished"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul
new file mode 100644
index 000000000..d910adc08
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0012_check_basic_staging.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download with staging, and finished"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1"
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul b/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul
new file mode 100644
index 000000000..c3f024c73
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0013_check_no_updates.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check and no updates found"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_NO_UPDATES_FOUND,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?noUpdates=1";
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul b/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul
new file mode 100644
index 000000000..f399a0096
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0014_check_error_xml_malformed.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check and error (xml malformed)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?xmlMalformed=1";
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul
new file mode 100644
index 000000000..1040c19e3
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0061_check_verifyFailPartial_noComplete.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download, and errors (partial patch with an invalid size)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&partialPatchOnly=1" +
+ "&invalidPartialSize=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul b/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul
new file mode 100644
index 000000000..9221a4b98
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0062_check_verifyFailComplete_noPartial.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download, and errors (complete patch with an invalid size)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&completePatchOnly=1" +
+ "&invalidCompleteSize=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul
new file mode 100644
index 000000000..8da5c7e97
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0063_check_verifyFailPartialComplete.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download, and errors (partial and complete patches with invalid sizes)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&invalidPartialSize=1" +
+ "&invalidCompleteSize=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul
new file mode 100644
index 000000000..db0f33d2f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0064_check_verifyFailPartial_successComplete.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check, basic, download, and finished (partial patch with an invalid size and successful complete patch)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&invalidPartialSize=1" +
+ getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul
new file mode 100644
index 000000000..736df13a3
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0071_notify_verifyFailPartial_noComplete.xul
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: errors (partial patch with an invalid size)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("partial", null, null, null, "1234", null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version,
+ Services.appinfo.platformVersion);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_DOWNLOADING);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul b/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul
new file mode 100644
index 000000000..cafab4d27
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0072_notify_verifyFailComplete_noPartial.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: errors (complete patch with an invalid size)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, "1234", null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_DOWNLOADING);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul
new file mode 100644
index 000000000..c1db983a3
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0073_notify_verifyFailPartialComplete.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: errors (partial and complete patches with invalid sizes)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("partial", null, null, null, "1234", null,
+ STATE_DOWNLOADING) +
+ getLocalPatchString("complete", null, null, null, "1234",
+ "false");
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_DOWNLOADING);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul b/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul
new file mode 100644
index 000000000..2c28da768
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0074_notify_verifyFailPartial_successComplete.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: finishedBackground (partial patch with an invalid size and successful complete patch)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("partial", null, null, null, "1234", null,
+ STATE_DOWNLOADING) +
+ getLocalPatchString("complete", null, null, null, null,
+ "false");
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_DOWNLOADING);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul b/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul
new file mode 100644
index 000000000..10c34f63b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0081_error_patchApplyFailure_partial_only.xul
@@ -0,0 +1,53 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: errors (partial only patch apply failure)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul b/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul
new file mode 100644
index 000000000..2c4b389f4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0082_error_patchApplyFailure_complete_only.xul
@@ -0,0 +1,52 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: errors (complete only patch apply failure)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
new file mode 100644
index 000000000..01adb1c3d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0083_error_patchApplyFailure_partial_complete.xul
@@ -0,0 +1,67 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: error patching, download, and finished (partial failed and download complete)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERROR_PATCHING,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING,
+ extraStartFunction: createContinueFile
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1",
+ extraStartFunction: removeContinueFile
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ removeContinueFile();
+
+ // Specify the url to update.sjs with a slowDownloadMar param so the ui can
+ // load before the download completes.
+ let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", slowDownloadURL, null, null,
+ null, "false");
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
new file mode 100644
index 000000000..2e0c2b41e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0084_error_patchApplyFailure_verify_failed.xul
@@ -0,0 +1,68 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: error patching, download, and errors (partial failed and download complete verification failure)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERROR_PATCHING,
+ buttonClick: "next"
+}, {
+ pageid: PAGEID_DOWNLOADING,
+ extraStartFunction: createContinueFile
+}, {
+ pageid: PAGEID_ERRORS,
+ buttonClick: "finish",
+ extraStartFunction: removeContinueFile
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ removeContinueFile();
+
+ // Specify the url to update.sjs with a slowDownloadMar param so the ui can
+ // load before the download completes.
+ let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", slowDownloadURL, "MD5",
+ null, "1234",
+ "false");
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_CRC_ERROR);
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul
new file mode 100644
index 000000000..fc83505f9
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0085_error_patchApplyFailure_partial_complete_staging.xul
@@ -0,0 +1,94 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: error patching, download with staging, and finished (partial failed and download complete), with fast MAR download"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+// This test forces the download to complete before the "next" button on the
+// errorpatching wizard page is clicked. This is done by creating the continue
+// file when the wizard loads to start the download, then clicking the "next"
+// button in the download's onStopRequest event listener.
+
+const testDownloadListener = {
+ onStartRequest(aRequest, aContext) { },
+
+ onProgress(aRequest, aContext, aProgress, aMaxProgress) { },
+
+ onStatus(aRequest, aContext, aStatus, aStatusText) { },
+
+ onStopRequest(aRequest, aContext, aStatus) {
+ debugDump("clicking errorpatching page next button");
+ gDocElem.getButton("next").click();
+ gAUS.removeDownloadListener(this);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+ Ci.nsIProgressEventSink])
+};
+
+let TESTS = [ {
+ pageid: PAGEID_ERROR_PATCHING,
+ extraCheckFunction: createContinueFile
+}, {
+ pageid: PAGEID_DOWNLOADING
+}, {
+ pageid: PAGEID_FINISHED,
+ buttonClick: "extra1",
+ extraStartFunction: removeContinueFile
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ removeContinueFile();
+
+ // Specify the url to update.sjs with a slowDownloadMar param so the ui can
+ // load before the download completes.
+ let slowDownloadURL = URL_HTTP_UPDATE_XML + "?slowDownloadMar=1";
+ let patches = getLocalPatchString("partial", null, null, null, null, null,
+ STATE_PENDING) +
+ getLocalPatchString("complete", slowDownloadURL, null, null,
+ null, "false");
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, "false");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_FAILED_READ_ERROR);
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+
+ reloadUpdateManagerData();
+
+ testPostUpdateProcessing();
+
+ gAUS.addDownloadListener(testDownloadListener);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul
new file mode 100644
index 000000000..a0b29ddea
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0092_finishedBackground.xul
@@ -0,0 +1,55 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: finished background"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version,
+ Services.appinfo.platformVersion);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, "pending",
+ "The active update should have a state of pending");
+
+ gUP.showUpdateDownloaded(gUpdateManager.activeUpdate);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul b/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul
new file mode 100644
index 000000000..6db5b9897
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0093_restartNotification.xul
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: restart notification pref promptWaitTime"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, STATE_PENDING,
+ "The active update should have a state of " + STATE_PENDING);
+
+ ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " +
+ "update's promptWaitTime attribute value was set from the " +
+ PREF_APP_UPDATE_PROMPTWAITTIME + " preference");
+
+ gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul b/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul
new file mode 100644
index 000000000..6e72a42c1
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0094_restartNotification_remote.xul
@@ -0,0 +1,60 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: restart notification xml promptWaitTime"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version, null,
+ null, null, null, null, false,
+ null, false, false, false, 1);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, STATE_PENDING,
+ "The active update should have a state of " + STATE_PENDING);
+
+ ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " +
+ "update's promptWaitTime attribute value was set by the XML");
+
+ gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true);
+
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul b/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul
new file mode 100644
index 000000000..5b1b826a5
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0095_restartNotification_remoteInvalidNumber.xul
@@ -0,0 +1,66 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: restart notification xml promptWaitTime with invalid number"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version,
+ null, null,
+ null, null, null,
+ null, false, null,
+ false, false,
+ false, "invalidNumber");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, STATE_PENDING,
+ "The active update should have a state of " + STATE_PENDING);
+
+ ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " +
+ "update's promptWaitTime attribute value was set from the " +
+ PREF_APP_UPDATE_PROMPTWAITTIME + " preference");
+
+ gUP.showUpdateDownloaded(gUpdateManager.activeUpdate, true);
+
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul
new file mode 100644
index 000000000..b86861012
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0096_restartNotification_stagedBackground.xul
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: restart notification staged w/o service"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_APPLIED);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version,
+ Services.appinfo.platformVersion);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_APPLIED);
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, STATE_APPLIED,
+ "The active update should have a state of " + STATE_APPLIED);
+
+ ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " +
+ "update's promptWaitTime attribute value was set from the " +
+ PREF_APP_UPDATE_PROMPTWAITTIME + " preference");
+
+ gUpdateManager.refreshUpdateStatus();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul b/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul
new file mode 100644
index 000000000..9f7a602c4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0097_restartNotification_stagedServiceBackground.xul
@@ -0,0 +1,65 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: restart notification staged with service"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ let patches = getLocalPatchString("complete", null, null, null, null, null,
+ STATE_APPLIED_SVC);
+ let updates = getLocalUpdateString(patches, null, null, null,
+ Services.appinfo.version,
+ Services.appinfo.platformVersion);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ writeStatusFile(STATE_APPLIED_SVC);
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true);
+
+ reloadUpdateManagerData();
+
+ is(gUpdateManager.activeUpdate.state, STATE_APPLIED_SVC,
+ "The active update should have a state of " + STATE_APPLIED_SVC);
+
+ ok(gUpdateManager.activeUpdate.promptWaitTime == 1, "Checking that the " +
+ "update's promptWaitTime attribute value was set from the " +
+ PREF_APP_UPDATE_PROMPTWAITTIME + " preference");
+
+ gUpdateManager.refreshUpdateStatus();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul b/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul
new file mode 100644
index 000000000..faa60c08b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0101_background_restartNotification.xul
@@ -0,0 +1,46 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: background finish with a background download"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gAUS.notify(null);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul b/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul
new file mode 100644
index 000000000..3e6f0fec8
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0102_background_restartNotification_staging.xul
@@ -0,0 +1,49 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: background finish with a background download and update staging"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gAUS.notify(null);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul b/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul
new file mode 100644
index 000000000..c60a9fe49
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0103_background_restartNotification_stagingService.xul
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: background finish with a background download and update staging and servicefs"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FINISHED_BKGRD,
+ buttonClick: "extra1"
+} ];
+
+gUseTestUpdater = true;
+
+function runTest() {
+ debugDump("entering");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, true);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 1);
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gAUS.notify(null);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul b/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul
new file mode 100644
index 000000000..adca621d9
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0111_neverButton_basic.xul
@@ -0,0 +1,61 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update check and basic (never button test)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+gPrefToCheck = PREFBRANCH_APP_UPDATE_NEVER + Services.appinfo.version;
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_FOUND_BASIC,
+ extraDelayedCheckFunction: checkPrefHasUserValue,
+ prefHasUserValue: false,
+ neverButton: true,
+ buttonClick: "extra2"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showNever=1&showDetails=1" +
+ getVersionParams();
+ setUpdateURL(url);
+
+ // add the never preference for this version to verify that checking for
+ // updates clears the preference.
+ Services.prefs.setBoolPref(gPrefToCheck, true)
+
+ gUP.checkForUpdates();
+}
+
+function finishTest() {
+ checkPrefHasUserValue(true);
+ finishTestDefault();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul b/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul
new file mode 100644
index 000000000..89dd55ea1
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0113_showNeverForVersionRemovedWithPref.xul
@@ -0,0 +1,58 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: update available with never pref and without showNeverForVersion"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+gPrefToCheck = PREFBRANCH_APP_UPDATE_NEVER + Services.appinfo.version;
+
+const TESTS = [ {
+ pageid: PAGEID_FOUND_BASIC,
+ extraDelayedCheckFunction: checkPrefHasUserValue,
+ prefHasUserValue: true,
+ buttonClick: "extra1"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" +
+ getVersionParams();
+ setUpdateURL(url);
+
+ // add the never preference for this version to verify that checking for
+ // updates clears the preference.
+ Services.prefs.setBoolPref(gPrefToCheck, true)
+
+ gAUS.notify(null);
+}
+
+function finishTest() {
+ Services.prefs.clearUserPref(gPrefToCheck)
+ finishTestDefault();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul b/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul
new file mode 100644
index 000000000..13798e5c9
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0151_notify_backgroundCheckError.xul
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test notification when multiple background check errors occur (bug 595455)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_ERROR_EXTRA,
+ extraDelayedCheckFunction: checkErrorExtraPage,
+ shouldBeHidden: false,
+ displayedTextElem: "bgErrorLabel",
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?xmlMalformed=1";
+ setUpdateURL(url);
+
+ errorsPrefObserver.init(PREF_APP_UPDATE_BACKGROUNDERRORS,
+ PREF_APP_UPDATE_BACKGROUNDMAXERRORS);
+
+ gAUS.notify(null);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul b/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul
new file mode 100644
index 000000000..04e613418
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0152_notify_backgroundCheckOfflineRetry.xul
@@ -0,0 +1,96 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test that an update check that fails due to being offline is performed after going online"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_FOUND_BASIC,
+ buttonClick: "extra1"
+} ];
+
+const NETWORK_ERROR_OFFLINE = 111;
+var gProxyPrefValue;
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, false);
+
+ Services.io.offline = true;
+ gProxyPrefValue = Services.prefs.getIntPref("network.proxy.type");
+ Services.prefs.setIntPref("network.proxy.type", 0);
+
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function resetOffline() {
+ Services.prefs.setIntPref("network.proxy.type", gProxyPrefValue);
+ Services.io.offline = false;
+}
+
+/* Update check listener */
+const updateCheckListener = {
+ onProgress: function UCL_onProgress(aRequest, aPosition, aTotalSize) {
+ },
+
+ onCheckComplete: function UCL_onCheckComplete(aRequest, aUpdates, aUpdateCount) {
+ let status = aRequest.status;
+ if (status == 0) {
+ status = aRequest.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ debugDump("url = " + aRequest.channel.originalURI.spec + ", " +
+ "request.status = " + status + ", " +
+ "updateCount = " + aUpdateCount);
+ ok(false, "Unexpected updateCheckListener::onCheckComplete called");
+ },
+
+ onError: function UCL_onError(aRequest, aUpdate) {
+ let status = aRequest.status;
+ if (status == 0) {
+ status = aRequest.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ is(status, Cr.NS_ERROR_OFFLINE,
+ "checking the request status value");
+ is(aUpdate.errorCode, NETWORK_ERROR_OFFLINE,
+ "checking the update error code");
+ debugDump("url = " + aRequest.channel.originalURI.spec + ", " +
+ "request.status = " + status + ", " +
+ "update.statusText = " +
+ (aUpdate.statusText ? aUpdate.statusText : "null"));
+ gAUS.onError(aRequest, aUpdate);
+ // Use a timeout to allow the XHR to complete
+ SimpleTest.executeSoon(resetOffline);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener])
+};
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul b/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul
new file mode 100644
index 000000000..c8e8d837b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0161_check_unsupported.xul
@@ -0,0 +1,50 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test checking for updates when system is no longer supported (bug 843497)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_UNSUPPORTED,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ // When checking manually the unsupported page should still be shown even if
+ // it was shown previously.
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true);
+
+ let url = URL_HTTP_UPDATE_XML + "?unsupported=1";
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul b/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul
new file mode 100644
index 000000000..d88d2092d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0162_notify_unsupported.xul
@@ -0,0 +1,44 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Test notification of updates when system is no longer supported (bug 843497)"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_UNSUPPORTED,
+ buttonClick: "finish"
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let url = URL_HTTP_UPDATE_XML + "?unsupported=1";
+ setUpdateURL(url);
+
+ gAUS.notify(null);
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul b/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul
new file mode 100644
index 000000000..142c02baa
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0171_check_noPerms_manual.xul
@@ -0,0 +1,64 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: manual"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_CHECKING
+}, {
+ pageid: PAGEID_MANUAL_UPDATE,
+ buttonClick: "finish",
+ extraCheckFunction: getWriteTestFile
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let file = getWriteTestFile();
+ file.create(file.NORMAL_FILE_TYPE, 0o444);
+ file.fileAttributesWin |= file.WFA_READONLY;
+ file.fileAttributesWin &= ~file.WFA_READWRITE;
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1" + getVersionParams();
+ setUpdateURL(url);
+
+ gUP.checkForUpdates();
+}
+
+function getWriteTestFile() {
+ let file = getAppBaseDir();
+ file.append(FILE_UPDATE_TEST);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ if (file.exists()) {
+ file.fileAttributesWin |= file.WFA_READWRITE;
+ file.fileAttributesWin &= ~file.WFA_READONLY;
+ file.remove(true);
+ }
+ return file;
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul b/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul
new file mode 100644
index 000000000..6784b9c90
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_0172_notify_noPerms_manual.xul
@@ -0,0 +1,63 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Update Wizard pages: manual"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTestDefault();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+const TESTS = [ {
+ pageid: PAGEID_MANUAL_UPDATE,
+ buttonClick: "finish",
+ extraCheckFunction: getWriteTestFile
+} ];
+
+function runTest() {
+ debugDump("entering");
+
+ let file = getWriteTestFile();
+ file.create(file.NORMAL_FILE_TYPE, 0o444);
+ file.fileAttributesWin |= file.WFA_READONLY;
+ file.fileAttributesWin &= ~file.WFA_READWRITE;
+
+ let url = URL_HTTP_UPDATE_XML + "?showDetails=1&showPrompt=1" +
+ getVersionParams();
+ setUpdateURL(url);
+
+ gAUS.checkForBackgroundUpdates();
+}
+
+function getWriteTestFile() {
+ let file = getAppBaseDir();
+ file.append(FILE_UPDATE_TEST);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ if (file.exists()) {
+ file.fileAttributesWin |= file.WFA_READWRITE;
+ file.fileAttributesWin &= ~file.WFA_READONLY;
+ file.remove(true);
+ }
+ return file;
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul b/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul
new file mode 100644
index 000000000..a55263eb2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/test_9999_cleanup.xul
@@ -0,0 +1,112 @@
+<?xml version="1.0"?>
+<!--
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Removes files and preferences for previous application update tests in case
+ * any of them had a fatal error. The test name ensures that it will run after
+ * all other tests as long as the test naming uses the same format as the
+ * existing tests.
+ */
+-->
+
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Application Update test cleanup"
+ xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
+ onload="runTest();">
+<script type="application/javascript"
+ src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"/>
+<script type="application/javascript"
+ src="utils.js"/>
+
+<script type="application/javascript">
+<![CDATA[
+
+/**
+ * If the application update tests left behind any of the files it uses it could
+ * be a very bad thing. The purpose of this test is to prevent that from
+ * happening.
+ */
+function runTest() {
+ debugDump("entering");
+
+ SimpleTest.waitForExplicitFinish();
+
+ if (DEBUG_AUS_TEST) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true);
+ }
+
+ closeUpdateWindow();
+
+ // Always leave the app.update.enabled and app.update.staging.enabled
+ // preferences set to false when cleaning up.
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, false);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
+
+ resetFiles();
+ removeUpdateDirsAndFiles();
+ reloadUpdateManagerData();
+
+ let file = getUpdatesXMLFile(true);
+ ok(!file.exists(), file.path + " should not exist");
+
+ file = getUpdatesXMLFile(false);
+ ok(!file.exists(), file.path + " should not exist");
+
+ let dir = getUpdatesDir();
+
+ file = dir.clone();
+ file.append(FILE_UPDATE_STATUS);
+ ok(!file.exists(), file.path + " should not exist");
+
+ file = dir.clone();
+ file.append(FILE_UPDATE_MAR);
+ ok(!file.exists(), file.path + " should not exist");
+
+ cleanupRestoreUpdaterBackup();
+}
+
+/**
+ * After all tests finish this will repeatedly attempt to restore the real
+ * updater if it exists and then call finishTest after the restore is
+ * successful.
+ */
+function cleanupRestoreUpdaterBackup() {
+ debugDump("entering");
+
+ try {
+ // Windows debug builds keep the updater file in use for a short period of
+ // time after the updater process exits.
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo("Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " + e);
+ SimpleTest.executeSoon(cleanupRestoreUpdaterBackup);
+ return;
+ }
+
+ SimpleTest.executeSoon(finishTest);
+}
+
+function finishTest() {
+ debugDump("entering");
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LOG)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_LOG);
+ }
+ SimpleTest.finish();
+}
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+ <p id="display"></p>
+ <div id="content" style="display: none"></div>
+ <pre id="test"></pre>
+</body>
+</window>
diff --git a/toolkit/mozapps/update/tests/chrome/update.sjs b/toolkit/mozapps/update/tests/chrome/update.sjs
new file mode 100644
index 000000000..78bb1b93f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/update.sjs
@@ -0,0 +1,194 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Server side http server script for application update tests.
+ */
+
+const { classes: Cc, interfaces: Ci } = Components;
+
+const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data/";
+
+function getTestDataFile(aFilename) {
+ let file = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).get("CurWorkD", Ci.nsILocalFile);
+ let pathParts = REL_PATH_DATA.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ file.append(pathParts[i]);
+ }
+ if (aFilename) {
+ file.append(aFilename);
+ }
+ return file;
+}
+
+function loadHelperScript() {
+ let scriptFile = getTestDataFile("sharedUpdateXML.js");
+ let io = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService2);
+ let scriptSpec = io.newFileURI(scriptFile).spec;
+ let scriptloader = Cc["@mozilla.org/moz/jssubscript-loader;1"].
+ getService(Ci.mozIJSSubScriptLoader);
+ scriptloader.loadSubScript(scriptSpec, this);
+}
+loadHelperScript();
+
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
+const URL_HTTP_UPDATE_SJS = URL_HOST + URL_PATH_UPDATE_XML;
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+
+const SLOW_MAR_DOWNLOAD_INTERVAL = 100;
+var gTimer;
+
+function handleRequest(aRequest, aResponse) {
+ let params = { };
+ if (aRequest.queryString) {
+ params = parseQueryString(aRequest.queryString);
+ }
+
+ let statusCode = params.statusCode ? parseInt(params.statusCode) : 200;
+ let statusReason = params.statusReason ? params.statusReason : "OK";
+ aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason);
+ aResponse.setHeader("Cache-Control", "no-cache", false);
+
+ // When a mar download is started by the update service it can finish
+ // downloading before the ui has loaded. By specifying a serviceURL for the
+ // update patch that points to this file and has a slowDownloadMar param the
+ // mar will be downloaded asynchronously which will allow the ui to load
+ // before the download completes.
+ if (params.slowDownloadMar) {
+ aResponse.processAsync();
+ aResponse.setHeader("Content-Type", "binary/octet-stream");
+ aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+ var continueFile = getTestDataFile("continue");
+ var contents = readFileBytes(getTestDataFile(FILE_SIMPLE_MAR));
+ gTimer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ gTimer.initWithCallback(function(aTimer) {
+ if (continueFile.exists()) {
+ gTimer.cancel();
+ aResponse.write(contents);
+ aResponse.finish();
+ }
+ }, SLOW_MAR_DOWNLOAD_INTERVAL, Ci.nsITimer.TYPE_REPEATING_SLACK);
+ return;
+ }
+
+ if (params.uiURL) {
+ let remoteType = "";
+ if (!params.remoteNoTypeAttr && params.uiURL == "BILLBOARD") {
+ remoteType = " " + params.uiURL.toLowerCase() + "=\"1\"";
+ }
+ aResponse.write("<html><head><meta http-equiv=\"content-type\" content=" +
+ "\"text/html; charset=utf-8\"></head><body" +
+ remoteType + ">" + params.uiURL +
+ "<br><br>this is a test mar that will not affect your " +
+ "build.</body></html>");
+ return;
+ }
+
+ if (params.xmlMalformed) {
+ aResponse.write("xml error");
+ return;
+ }
+
+ if (params.noUpdates) {
+ aResponse.write(getRemoteUpdatesXMLString(""));
+ return;
+ }
+
+ if (params.unsupported) {
+ aResponse.write(getRemoteUpdatesXMLString(" <update type=\"major\" " +
+ "unsupported=\"true\" " +
+ "detailsURL=\"" + URL_HOST +
+ "\"></update>\n"));
+ return;
+ }
+
+ let size;
+ let patches = "";
+ if (!params.partialPatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
+ patches += getRemotePatchString("complete", SERVICE_URL, "SHA512",
+ SHA512_HASH_SIMPLE_MAR, size);
+ }
+
+ if (!params.completePatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
+ patches += getRemotePatchString("partial", SERVICE_URL, "SHA512",
+ SHA512_HASH_SIMPLE_MAR, size);
+ }
+
+ let type = params.type ? params.type : "major";
+ let name = params.name ? params.name : "App Update Test";
+ let appVersion = params.appVersion ? params.appVersion : "999999.9";
+ let displayVersion = params.displayVersion ? params.displayVersion
+ : "version " + appVersion;
+ let buildID = params.buildID ? params.buildID : "01234567890123";
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+// let detailsURL = params.showDetails ? URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS" : null;
+ let detailsURL = URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS";
+ let showPrompt = params.showPrompt ? "true" : null;
+ let showNever = params.showNever ? "true" : null;
+ let promptWaitTime = params.promptWaitTime ? params.promptWaitTime : null;
+
+ let updates = getRemoteUpdateString(patches, type, "App Update Test",
+ displayVersion, appVersion, buildID,
+ detailsURL, showPrompt, showNever,
+ promptWaitTime);
+ aResponse.write(getRemoteUpdatesXMLString(updates));
+}
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param aQueryString
+ * The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ * queryString.
+ */
+function parseQueryString(aQueryString) {
+ let paramArray = aQueryString.split("&");
+ let regex = /^([^=]+)=(.*)$/;
+ let params = {};
+ for (let i = 0, sz = paramArray.length; i < sz; i++) {
+ let match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw "Bad parameter in queryString! '" + paramArray[i] + "'";
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
+
+/**
+ * Reads the binary contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The contents of the file as a string.
+ */
+function readFileBytes(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(aFile, -1, -1, false);
+ let bis = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ bis.setInputStream(fis);
+ let data = [];
+ let count = fis.available();
+ while (count > 0) {
+ let bytes = bis.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (bytes.length == 0) {
+ throw "Nothing read from input stream!";
+ }
+ }
+ data.join('');
+ fis.close();
+ return data.toString();
+}
diff --git a/toolkit/mozapps/update/tests/chrome/utils.js b/toolkit/mozapps/update/tests/chrome/utils.js
new file mode 100644
index 000000000..31d0d2e5a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/chrome/utils.js
@@ -0,0 +1,1011 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test Definition
+ *
+ * Most tests can use an array named TESTS that will perform most if not all of
+ * the necessary checks. Each element in the array must be an object with the
+ * following possible properties. Additional properties besides the ones listed
+ * below can be added as needed.
+ *
+ * overrideCallback (optional)
+ * The function to call for the next test. This is typically called when the
+ * wizard page changes but can also be called for other events by the previous
+ * test. If this property isn't defined then the defaultCallback function will
+ * be called. If this property is defined then all other properties are
+ * optional.
+ *
+ * pageid (required unless overrideCallback is specified)
+ * The expected pageid for the wizard. This property is required unless the
+ * overrideCallback property is defined.
+ *
+ * extraStartFunction (optional)
+ * The function to call at the beginning of the defaultCallback function. If
+ * the function returns true the defaultCallback function will return early
+ * which allows waiting for a specific condition to be evaluated in the
+ * function specified in the extraStartFunction property before continuing
+ * with the test.
+ *
+ * extraCheckFunction (optional)
+ * The function to call to perform extra checks in the defaultCallback
+ * function.
+ *
+ * extraDelayedCheckFunction (optional)
+ * The function to call to perform extra checks in the delayedDefaultCallback
+ * function.
+ *
+ * buttonStates (optional)
+ * A javascript object representing the expected hidden and disabled attribute
+ * values for the buttons of the current wizard page. The values are checked
+ * in the delayedDefaultCallback function. For information about the structure
+ * of this object refer to the getExpectedButtonStates and checkButtonStates
+ * functions.
+ *
+ * buttonClick (optional)
+ * The current wizard page button to click at the end of the
+ * delayedDefaultCallback function. If the buttonClick property is defined
+ * then the extraDelayedFinishFunction property can't be specified due to race
+ * conditions in some of the tests and if both of them are specified the test
+ * will intentionally throw.
+ *
+ * extraDelayedFinishFunction (optional)
+ * The function to call at the end of the delayedDefaultCallback function.
+ * If the extraDelayedFinishFunction property is defined then the buttonClick
+ * property can't be specified due to race conditions in some of the tests and
+ * if both of them are specified the test will intentionally throw.
+ *
+ * ranTest (should not be specified)
+ * When delayedDefaultCallback is called a property named ranTest is added to
+ * the current test so it is possible to verify that each test in the TESTS
+ * array has ran.
+ *
+ * prefHasUserValue (optional)
+ * For comparing the expected value defined by this property with the return
+ * value of prefHasUserValue using gPrefToCheck for the preference name in the
+ * checkPrefHasUserValue function.
+ */
+
+'use strict';
+
+/* globals TESTS, runTest, finishTest */
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
+ utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+
+const IS_MACOSX = ("nsILocalFileMac" in Ci);
+const IS_WIN = ("@mozilla.org/windows-registry-key;1" in Cc);
+
+// The tests have to use the pageid instead of the pageIndex due to the
+// app update wizard's access method being random.
+const PAGEID_DUMMY = "dummy"; // Done
+const PAGEID_CHECKING = "checking"; // Done
+const PAGEID_NO_UPDATES_FOUND = "noupdatesfound"; // Done
+const PAGEID_MANUAL_UPDATE = "manualUpdate"; // Done
+const PAGEID_UNSUPPORTED = "unsupported"; // Done
+const PAGEID_FOUND_BASIC = "updatesfoundbasic"; // Done
+const PAGEID_DOWNLOADING = "downloading"; // Done
+const PAGEID_ERRORS = "errors"; // Done
+const PAGEID_ERROR_EXTRA = "errorextra"; // Done
+const PAGEID_ERROR_PATCHING = "errorpatching"; // Done
+const PAGEID_FINISHED = "finished"; // Done
+const PAGEID_FINISHED_BKGRD = "finishedBackground"; // Done
+
+const UPDATE_WINDOW_NAME = "Update:Wizard";
+
+const URL_HOST = "http://example.com";
+const URL_PATH_UPDATE_XML = "/chrome/toolkit/mozapps/update/tests/chrome/update.sjs";
+const REL_PATH_DATA = "chrome/toolkit/mozapps/update/tests/data";
+
+// These two URLs must not contain parameters since tests add their own
+// test specific parameters.
+const URL_HTTP_UPDATE_XML = URL_HOST + URL_PATH_UPDATE_XML;
+const URL_HTTPS_UPDATE_XML = "https://example.com" + URL_PATH_UPDATE_XML;
+
+const URI_UPDATE_PROMPT_DIALOG = "chrome://mozapps/content/update/updates.xul";
+
+const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
+const PREF_APP_UPDATE_LASTUPDATETIME = "app.update.lastUpdateTime.background-update-timer";
+
+const LOG_FUNCTION = info;
+
+const BIN_SUFFIX = (IS_WIN ? ".exe" : "");
+const FILE_UPDATER_BIN = "updater" + (IS_MACOSX ? ".app" : BIN_SUFFIX);
+const FILE_UPDATER_BIN_BAK = FILE_UPDATER_BIN + ".bak";
+
+var gURLData = URL_HOST + "/" + REL_PATH_DATA + "/";
+
+var gTestTimeout = 240000; // 4 minutes
+var gTimeoutTimer;
+
+// The number of SimpleTest.executeSoon calls to perform when waiting on an
+// update window to close before giving up.
+const CLOSE_WINDOW_TIMEOUT_MAXCOUNT = 10;
+// Counter for the SimpleTest.executeSoon when waiting on an update window to
+// close before giving up.
+var gCloseWindowTimeoutCounter = 0;
+
+// The following vars are for restoring previous preference values (if present)
+// when the test finishes.
+var gAppUpdateEnabled; // app.update.enabled
+var gAppUpdateServiceEnabled; // app.update.service.enabled
+var gAppUpdateStagingEnabled; // app.update.staging.enabled
+var gAppUpdateURLDefault; // app.update.url (default prefbranch)
+
+var gTestCounter = -1;
+var gWin;
+var gDocElem;
+var gPrefToCheck;
+var gUseTestUpdater = false;
+
+// Set to true to log additional information for debugging. To log additional
+// information for an individual test set DEBUG_AUS_TEST to true in the test's
+// onload function.
+var DEBUG_AUS_TEST = true;
+
+const DATA_URI_SPEC = "chrome://mochitests/content/chrome/toolkit/mozapps/update/tests/data/";
+/* import-globals-from ../data/shared.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
+
+/**
+ * The current test in TESTS array.
+ */
+this.__defineGetter__("gTest", function() {
+ return TESTS[gTestCounter];
+});
+
+/**
+ * The current test's callback. This will either return the callback defined in
+ * the test's overrideCallback property or defaultCallback if the
+ * overrideCallback property is undefined.
+ */
+this.__defineGetter__("gCallback", function() {
+ return gTest.overrideCallback ? gTest.overrideCallback
+ : defaultCallback;
+});
+
+/**
+ * nsIObserver for receiving window open and close notifications.
+ */
+const gWindowObserver = {
+ observe: function WO_observe(aSubject, aTopic, aData) {
+ let win = aSubject.QueryInterface(Ci.nsIDOMEventTarget);
+
+ if (aTopic == "domwindowclosed") {
+ if (win.location != URI_UPDATE_PROMPT_DIALOG) {
+ debugDump("domwindowclosed event for window not being tested - " +
+ "location: " + win.location + "... returning early");
+ return;
+ }
+ // Allow tests the ability to provide their own function (it must be
+ // named finishTest) for finishing the test.
+ try {
+ finishTest();
+ }
+ catch (e) {
+ finishTestDefault();
+ }
+ return;
+ }
+
+ win.addEventListener("load", function WO_observe_onLoad() {
+ win.removeEventListener("load", WO_observe_onLoad, false);
+ // Ignore windows other than the update UI window.
+ if (win.location != URI_UPDATE_PROMPT_DIALOG) {
+ debugDump("load event for window not being tested - location: " +
+ win.location + "... returning early");
+ return;
+ }
+
+ // The first wizard page should always be the dummy page.
+ let pageid = win.document.documentElement.currentPage.pageid;
+ if (pageid != PAGEID_DUMMY) {
+ // This should never happen but if it does this will provide a clue
+ // for diagnosing the cause.
+ ok(false, "Unexpected load event - pageid got: " + pageid +
+ ", expected: " + PAGEID_DUMMY + "... returning early");
+ return;
+ }
+
+ gWin = win;
+ gDocElem = gWin.document.documentElement;
+ gDocElem.addEventListener("pageshow", onPageShowDefault, false);
+ }, false);
+ }
+};
+
+/**
+ * Default test run function that can be used by most tests. This function uses
+ * protective measures to prevent the test from failing provided by
+ * |runTestDefaultWaitForWindowClosed| helper functions to prevent failure due
+ * to a previous test failure.
+ */
+function runTestDefault() {
+ debugDump("entering");
+
+ if (!("@mozilla.org/zipwriter;1" in Cc)) {
+ ok(false, "nsIZipWriter is required to run these tests");
+ return;
+ }
+
+ SimpleTest.waitForExplicitFinish();
+
+ runTestDefaultWaitForWindowClosed();
+}
+
+/**
+ * If an update window is found SimpleTest.executeSoon can callback before the
+ * update window is fully closed especially with debug builds. If an update
+ * window is found this function will call itself using SimpleTest.executeSoon
+ * up to the amount declared in CLOSE_WINDOW_TIMEOUT_MAXCOUNT until the update
+ * window has closed before continuing the test.
+ */
+function runTestDefaultWaitForWindowClosed() {
+ gCloseWindowTimeoutCounter++;
+ if (gCloseWindowTimeoutCounter > CLOSE_WINDOW_TIMEOUT_MAXCOUNT) {
+ try {
+ finishTest();
+ }
+ catch (e) {
+ finishTestDefault();
+ }
+ return;
+ }
+
+ // The update window should not be open at this time. If it is the call to
+ // |closeUpdateWindow| will close it and cause the test to fail.
+ if (closeUpdateWindow()) {
+ SimpleTest.executeSoon(runTestDefaultWaitForWindowClosed);
+ } else {
+ Services.ww.registerNotification(gWindowObserver);
+
+ gCloseWindowTimeoutCounter = 0;
+
+ setupFiles();
+ setupPrefs();
+ gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", "1");
+ removeUpdateDirsAndFiles();
+ reloadUpdateManagerData();
+ setupTimer(gTestTimeout);
+ SimpleTest.executeSoon(setupTestUpdater);
+ }
+}
+
+/**
+ * Default test finish function that can be used by most tests. This function
+ * uses protective measures to prevent the next test from failing provided by
+ * |finishTestDefaultWaitForWindowClosed| helper functions to prevent failure
+ * due to an update window being left open.
+ */
+function finishTestDefault() {
+ debugDump("entering");
+ if (gTimeoutTimer) {
+ gTimeoutTimer.cancel();
+ gTimeoutTimer = null;
+ }
+
+ if (gChannel) {
+ debugDump("channel = " + gChannel);
+ gChannel = null;
+ gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer);
+ }
+
+ verifyTestsRan();
+
+ resetPrefs();
+ gEnv.set("MOZ_TEST_SKIP_UPDATE_STAGE", "");
+ resetFiles();
+ removeUpdateDirsAndFiles();
+ reloadUpdateManagerData();
+
+ Services.ww.unregisterNotification(gWindowObserver);
+ if (gDocElem) {
+ gDocElem.removeEventListener("pageshow", onPageShowDefault, false);
+ }
+
+ finishTestRestoreUpdaterBackup();
+}
+
+/**
+ * nsITimerCallback for the timeout timer to cleanly finish a test if the Update
+ * Window doesn't close for a test. This allows the next test to run properly if
+ * a previous test fails.
+ *
+ * @param aTimer
+ * The nsITimer that fired.
+ */
+function finishTestTimeout(aTimer) {
+ ok(false, "Test timed out. Maximum time allowed is " + (gTestTimeout / 1000) +
+ " seconds");
+
+ try {
+ finishTest();
+ }
+ catch (e) {
+ finishTestDefault();
+ }
+}
+
+/**
+ * When a test finishes this will repeatedly attempt to restore the real updater
+ * for tests that use the test updater and then call
+ * finishTestDefaultWaitForWindowClosed after the restore is successful.
+ */
+function finishTestRestoreUpdaterBackup() {
+ if (gUseTestUpdater) {
+ try {
+ // Windows debug builds keep the updater file in use for a short period of
+ // time after the updater process exits.
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo("Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " + e);
+ SimpleTest.executeSoon(finishTestRestoreUpdaterBackup);
+ return;
+ }
+ }
+
+ finishTestDefaultWaitForWindowClosed();
+}
+
+/**
+ * If an update window is found SimpleTest.executeSoon can callback before the
+ * update window is fully closed especially with debug builds. If an update
+ * window is found this function will call itself using SimpleTest.executeSoon
+ * up to the amount declared in CLOSE_WINDOW_TIMEOUT_MAXCOUNT until the update
+ * window has closed before finishing the test.
+ */
+function finishTestDefaultWaitForWindowClosed() {
+ gCloseWindowTimeoutCounter++;
+ if (gCloseWindowTimeoutCounter > CLOSE_WINDOW_TIMEOUT_MAXCOUNT) {
+ SimpleTest.requestCompleteLog();
+ SimpleTest.finish();
+ return;
+ }
+
+ // The update window should not be open at this time. If it is the call to
+ // |closeUpdateWindow| will close it and cause the test to fail.
+ if (closeUpdateWindow()) {
+ SimpleTest.executeSoon(finishTestDefaultWaitForWindowClosed);
+ } else {
+ SimpleTest.finish();
+ }
+}
+
+/**
+ * Default callback for the wizard's documentElement pageshow listener. This
+ * will return early for event's where the originalTarget's nodeName is not
+ * wizardpage.
+ */
+function onPageShowDefault(aEvent) {
+ if (!gTimeoutTimer) {
+ debugDump("gTimeoutTimer is null... returning early");
+ return;
+ }
+
+ // Return early if the event's original target isn't for a wizardpage element.
+ // This check is necessary due to the remotecontent element firing pageshow.
+ if (aEvent.originalTarget.nodeName != "wizardpage") {
+ debugDump("only handles events with an originalTarget nodeName of " +
+ "|wizardpage|. aEvent.originalTarget.nodeName = " +
+ aEvent.originalTarget.nodeName + "... returning early");
+ return;
+ }
+
+ gTestCounter++;
+ gCallback(aEvent);
+}
+
+/**
+ * Default callback that can be used by most tests.
+ */
+function defaultCallback(aEvent) {
+ if (!gTimeoutTimer) {
+ debugDump("gTimeoutTimer is null... returning early");
+ return;
+ }
+
+ debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid +
+ ", aEvent.originalTarget.nodeName: " +
+ aEvent.originalTarget.nodeName);
+
+ if (gTest && gTest.extraStartFunction) {
+ debugDump("calling extraStartFunction " + gTest.extraStartFunction.name);
+ if (gTest.extraStartFunction(aEvent)) {
+ debugDump("extraStartFunction early return");
+ return;
+ }
+ }
+
+ is(gDocElem.currentPage.pageid, gTest.pageid,
+ "Checking currentPage.pageid equals " + gTest.pageid + " in pageshow");
+
+ // Perform extra checks if specified by the test
+ if (gTest.extraCheckFunction) {
+ debugDump("calling extraCheckFunction " + gTest.extraCheckFunction.name);
+ gTest.extraCheckFunction();
+ }
+
+ // The wizard page buttons' disabled and hidden attributes are set after the
+ // pageshow event so use executeSoon to allow them to be set so their disabled
+ // and hidden attribute values can be checked.
+ SimpleTest.executeSoon(delayedDefaultCallback);
+}
+
+/**
+ * Delayed default callback called using executeSoon in defaultCallback which
+ * allows the wizard page buttons' disabled and hidden attributes to be set
+ * before checking their values.
+ */
+function delayedDefaultCallback() {
+ if (!gTimeoutTimer) {
+ debugDump("gTimeoutTimer is null... returning early");
+ return;
+ }
+
+ if (!gTest) {
+ debugDump("gTest is null... returning early");
+ return;
+ }
+
+ debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid);
+
+ // Verify the pageid hasn't changed after executeSoon was called.
+ is(gDocElem.currentPage.pageid, gTest.pageid,
+ "Checking currentPage.pageid equals " + gTest.pageid + " after " +
+ "executeSoon");
+
+ checkButtonStates();
+
+ // Perform delayed extra checks if specified by the test
+ if (gTest.extraDelayedCheckFunction) {
+ debugDump("calling extraDelayedCheckFunction " +
+ gTest.extraDelayedCheckFunction.name);
+ gTest.extraDelayedCheckFunction();
+ }
+
+ // Used to verify that this test has been performed
+ gTest.ranTest = true;
+
+ if (gTest.buttonClick) {
+ debugDump("clicking " + gTest.buttonClick + " button");
+ if (gTest.extraDelayedFinishFunction) {
+ throw ("Tests cannot have a buttonClick and an extraDelayedFinishFunction property");
+ }
+ gDocElem.getButton(gTest.buttonClick).click();
+ } else if (gTest.extraDelayedFinishFunction) {
+ debugDump("calling extraDelayedFinishFunction " +
+ gTest.extraDelayedFinishFunction.name);
+ gTest.extraDelayedFinishFunction();
+ }
+}
+
+/**
+ * Gets the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests without creating it.
+ *
+ * @return nsILocalFile for the continue file.
+ */
+function getContinueFile() {
+ let continueFile = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties).
+ get("CurWorkD", Ci.nsILocalFile);
+ let continuePath = REL_PATH_DATA + "/continue";
+ let continuePathParts = continuePath.split("/");
+ for (let i = 0; i < continuePathParts.length; ++i) {
+ continueFile.append(continuePathParts[i]);
+ }
+ return continueFile;
+}
+
+/**
+ * Creates the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function createContinueFile() {
+ debugDump("creating 'continue' file for slow mar downloads");
+ writeFile(getContinueFile(), "");
+}
+
+/**
+ * Removes the continue file used to signal the mock http server to continue
+ * downloading for slow download mar file tests.
+ */
+function removeContinueFile() {
+ let continueFile = getContinueFile();
+ if (continueFile.exists()) {
+ debugDump("removing 'continue' file for slow mar downloads");
+ continueFile.remove(false);
+ }
+}
+
+/**
+ * Checks the wizard page buttons' disabled and hidden attributes values are
+ * correct. If an expected button id is not specified then the expected disabled
+ * and hidden attribute value is true.
+ */
+function checkButtonStates() {
+ debugDump("entering - TESTS[" + gTestCounter + "], pageid: " + gTest.pageid);
+
+ const buttonNames = ["extra1", "extra2", "back", "next", "finish", "cancel"];
+ let buttonStates = getExpectedButtonStates();
+ buttonNames.forEach(function(aButtonName) {
+ let button = gDocElem.getButton(aButtonName);
+ let hasHidden = aButtonName in buttonStates &&
+ "hidden" in buttonStates[aButtonName];
+ let hidden = hasHidden ? buttonStates[aButtonName].hidden : true;
+ let hasDisabled = aButtonName in buttonStates &&
+ "disabled" in buttonStates[aButtonName];
+ let disabled = hasDisabled ? buttonStates[aButtonName].disabled : true;
+ is(button.hidden, hidden, "Checking " + aButtonName + " button " +
+ "hidden attribute value equals " + (hidden ? "true" : "false"));
+ is(button.disabled, disabled, "Checking " + aButtonName + " button " +
+ "disabled attribute value equals " + (disabled ? "true" : "false"));
+ });
+}
+
+/**
+ * Returns the expected disabled and hidden attribute values for the buttons of
+ * the current wizard page.
+ */
+function getExpectedButtonStates() {
+ // Allow individual tests to override the expected button states.
+ if (gTest.buttonStates) {
+ return gTest.buttonStates;
+ }
+
+ switch (gTest.pageid) {
+ case PAGEID_CHECKING:
+ return {cancel: {disabled: false, hidden: false}};
+ case PAGEID_FOUND_BASIC:
+ if (gTest.neverButton) {
+ return {extra1: {disabled: false, hidden: false},
+ extra2: {disabled: false, hidden: false},
+ next: {disabled: false, hidden: false}};
+ }
+ return {extra1: {disabled: false, hidden: false},
+ next: {disabled: false, hidden: false}};
+ case PAGEID_DOWNLOADING:
+ return {extra1: {disabled: false, hidden: false}};
+ case PAGEID_NO_UPDATES_FOUND:
+ case PAGEID_MANUAL_UPDATE:
+ case PAGEID_UNSUPPORTED:
+ case PAGEID_ERRORS:
+ case PAGEID_ERROR_EXTRA:
+ return {finish: {disabled: false, hidden: false}};
+ case PAGEID_ERROR_PATCHING:
+ return {next: { disabled: false, hidden: false}};
+ case PAGEID_FINISHED:
+ case PAGEID_FINISHED_BKGRD:
+ return {extra1: { disabled: false, hidden: false},
+ finish: { disabled: false, hidden: false}};
+ }
+ return null;
+}
+
+/**
+ * Compares the return value of prefHasUserValue for the preference specified in
+ * gPrefToCheck with the value passed in the aPrefHasValue parameter or the
+ * value specified in the current test's prefHasUserValue property if
+ * aPrefHasValue is undefined.
+ *
+ * @param aPrefHasValue (optional)
+ * The expected value returned from prefHasUserValue for the preference
+ * specified in gPrefToCheck. If aPrefHasValue is undefined the value
+ * of the current test's prefHasUserValue property will be used.
+ */
+function checkPrefHasUserValue(aPrefHasValue) {
+ let prefHasUserValue = aPrefHasValue === undefined ? gTest.prefHasUserValue
+ : aPrefHasValue;
+ is(Services.prefs.prefHasUserValue(gPrefToCheck), prefHasUserValue,
+ "Checking prefHasUserValue for preference " + gPrefToCheck + " equals " +
+ (prefHasUserValue ? "true" : "false"));
+}
+
+/**
+ * Checks whether the link is hidden for a general background update check error
+ * or not on the errorextra page and that the app.update.backgroundErrors
+ * preference does not have a user value.
+ *
+ * @param aShouldBeHidden (optional)
+ * The expected value for the label's hidden attribute for the link. If
+ * aShouldBeHidden is undefined the value of the current test's
+ * shouldBeHidden property will be used.
+ */
+function checkErrorExtraPage(aShouldBeHidden) {
+ let shouldBeHidden = aShouldBeHidden === undefined ? gTest.shouldBeHidden
+ : aShouldBeHidden;
+ is(gWin.document.getElementById("errorExtraLinkLabel").hidden, shouldBeHidden,
+ "Checking errorExtraLinkLabel hidden attribute equals " +
+ (shouldBeHidden ? "true" : "false"));
+
+ is(gWin.document.getElementById(gTest.displayedTextElem).hidden, false,
+ "Checking " + gTest.displayedTextElem + " should not be hidden");
+
+ ok(!Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS),
+ "Preference " + PREF_APP_UPDATE_BACKGROUNDERRORS + " should not have a " +
+ "user value");
+}
+
+/**
+ * Gets the update version info for the update url parameters to send to
+ * update.sjs.
+ *
+ * @param aAppVersion (optional)
+ * The application version for the update snippet. If not specified the
+ * current application version will be used.
+ * @return The url parameters for the application and platform version to send
+ * to update.sjs.
+ */
+function getVersionParams(aAppVersion) {
+ let appInfo = Services.appinfo;
+ return "&appVersion=" + (aAppVersion ? aAppVersion : appInfo.version);
+}
+
+/**
+ * Verifies that all tests ran.
+ */
+function verifyTestsRan() {
+ debugDump("entering");
+
+ // Return early if there are no tests defined.
+ if (!TESTS) {
+ return;
+ }
+
+ gTestCounter = -1;
+ for (let i = 0; i < TESTS.length; ++i) {
+ gTestCounter++;
+ let test = TESTS[i];
+ let msg = "Checking if TESTS[" + i + "] test was performed... " +
+ "callback function name = " + gCallback.name + ", " +
+ "pageid = " + test.pageid;
+ ok(test.ranTest, msg);
+ }
+}
+
+/**
+ * Creates a backup of files the tests need to modify so they can be restored to
+ * the original file when the test has finished and then modifies the files.
+ */
+function setupFiles() {
+ // Backup the updater-settings.ini file if it exists by moving it.
+ let baseAppDir = getGREDir();
+ let updateSettingsIni = baseAppDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ if (updateSettingsIni.exists()) {
+ updateSettingsIni.moveTo(baseAppDir, FILE_UPDATE_SETTINGS_INI_BAK);
+ }
+ updateSettingsIni = baseAppDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI);
+ writeFile(updateSettingsIni, UPDATE_SETTINGS_CONTENTS);
+}
+
+/**
+ * For tests that use the test updater restores the backed up real updater if
+ * it exists and tries again on failure since Windows debug builds at times
+ * leave the file in use. After success moveRealUpdater is called to continue
+ * the setup of the test updater. For tests that don't use the test updater
+ * runTest will be called.
+ */
+function setupTestUpdater() {
+ if (!gUseTestUpdater) {
+ runTest();
+ return;
+ }
+
+ try {
+ restoreUpdaterBackup();
+ } catch (e) {
+ logTestInfo("Attempt to restore the backed up updater failed... " +
+ "will try again, Exception: " + e);
+ SimpleTest.executeSoon(setupTestUpdater);
+ return;
+ }
+ moveRealUpdater();
+}
+
+/**
+ * Backs up the real updater and tries again on failure since Windows debug
+ * builds at times leave the file in use. After success it will call
+ * copyTestUpdater to continue the setup of the test updater.
+ */
+function moveRealUpdater() {
+ try {
+ // Move away the real updater
+ let baseAppDir = getAppBaseDir();
+ let updater = baseAppDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updater.moveTo(baseAppDir, FILE_UPDATER_BIN_BAK);
+ } catch (e) {
+ logTestInfo("Attempt to move the real updater out of the way failed... " +
+ "will try again, Exception: " + e);
+ SimpleTest.executeSoon(moveRealUpdater);
+ return;
+ }
+
+ copyTestUpdater();
+}
+
+/**
+ * Copies the test updater so it can be used by tests and tries again on failure
+ * since Windows debug builds at times leave the file in use. After success it
+ * will call runTest to continue the test.
+ */
+function copyTestUpdater() {
+ try {
+ // Copy the test updater
+ let baseAppDir = getAppBaseDir();
+ let testUpdaterDir = Services.dirsvc.get("CurWorkD", Ci.nsILocalFile);
+ let relPath = REL_PATH_DATA;
+ let pathParts = relPath.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ testUpdaterDir.append(pathParts[i]);
+ }
+
+ let testUpdater = testUpdaterDir.clone();
+ testUpdater.append(FILE_UPDATER_BIN);
+ testUpdater.copyToFollowingLinks(baseAppDir, FILE_UPDATER_BIN);
+ } catch (e) {
+ logTestInfo("Attempt to copy the test updater failed... " +
+ "will try again, Exception: " + e);
+ SimpleTest.executeSoon(copyTestUpdater);
+ return;
+ }
+
+ runTest();
+}
+
+/**
+ * Restores the updater that was backed up. This is called in setupTestUpdater
+ * before the backup of the real updater is done in case the previous test
+ * failed to restore the updater, in finishTestDefaultWaitForWindowClosed when
+ * the test has finished, and in test_9999_cleanup.xul after all tests have
+ * finished.
+ */
+function restoreUpdaterBackup() {
+ let baseAppDir = getAppBaseDir();
+ let updater = baseAppDir.clone();
+ let updaterBackup = baseAppDir.clone();
+ updater.append(FILE_UPDATER_BIN);
+ updaterBackup.append(FILE_UPDATER_BIN_BAK);
+ if (updaterBackup.exists()) {
+ if (updater.exists()) {
+ updater.remove(true);
+ }
+ updaterBackup.moveTo(baseAppDir, FILE_UPDATER_BIN);
+ }
+}
+
+/**
+ * Sets the most common preferences used by tests to values used by the majority
+ * of the tests and when necessary saves the preference's original values if
+ * present so they can be set back to the original values when the test has
+ * finished.
+ */
+function setupPrefs() {
+ if (DEBUG_AUS_TEST) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true);
+ }
+
+ // Prevent nsIUpdateTimerManager from notifying nsIApplicationUpdateService
+ // to check for updates by setting the app update last update time to the
+ // current time minus one minute in seconds and the interval time to 12 hours
+ // in seconds.
+ let now = Math.round(Date.now() / 1000) - 60;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_LASTUPDATETIME, now);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_INTERVAL, 43200);
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) {
+ gAppUpdateEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_ENABLED);
+ }
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ENABLED)) {
+ gAppUpdateServiceEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED);
+ }
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, false);
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) {
+ gAppUpdateStagingEnabled = Services.prefs.getBoolPref(PREF_APP_UPDATE_STAGING_ENABLED);
+ }
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
+
+ Services.prefs.setIntPref(PREF_APP_UPDATE_IDLETIME, 0);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_PROMPTWAITTIME, 0);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL, 0);
+}
+
+/**
+ * Restores files that were backed up for the tests and general file cleanup.
+ */
+function resetFiles() {
+ // Restore the backed up updater-settings.ini if it exists.
+ let baseAppDir = getGREDir();
+ let updateSettingsIni = baseAppDir.clone();
+ updateSettingsIni.append(FILE_UPDATE_SETTINGS_INI_BAK);
+ if (updateSettingsIni.exists()) {
+ updateSettingsIni.moveTo(baseAppDir, FILE_UPDATE_SETTINGS_INI);
+ }
+
+ // Not being able to remove the "updated" directory will not adversely affect
+ // subsequent tests so wrap it in a try block and don't test whether its
+ // removal was successful.
+ let updatedDir;
+ if (IS_MACOSX) {
+ updatedDir = getUpdatesDir();
+ updatedDir.append(DIR_PATCH);
+ } else {
+ updatedDir = getAppBaseDir();
+ }
+ updatedDir.append(DIR_UPDATED);
+ if (updatedDir.exists()) {
+ try {
+ removeDirRecursive(updatedDir);
+ }
+ catch (e) {
+ logTestInfo("Unable to remove directory. Path: " + updatedDir.path +
+ ", Exception: " + e);
+ }
+ }
+}
+
+/**
+ * Resets the most common preferences used by tests to their original values.
+ */
+function resetPrefs() {
+ if (gAppUpdateURLDefault) {
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_URL, gAppUpdateURLDefault);
+ }
+
+ if (gAppUpdateEnabled !== undefined) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, gAppUpdateEnabled);
+ } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_ENABLED)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_ENABLED);
+ }
+
+ if (gAppUpdateServiceEnabled !== undefined) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED, gAppUpdateServiceEnabled);
+ } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SERVICE_ENABLED)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_SERVICE_ENABLED);
+ }
+
+ if (gAppUpdateStagingEnabled !== undefined) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, gAppUpdateStagingEnabled);
+ } else if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_STAGING_ENABLED)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_STAGING_ENABLED);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_IDLETIME)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_IDLETIME);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_PROMPTWAITTIME)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_PROMPTWAITTIME);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_URL_DETAILS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_URL_DETAILS);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_LOG)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_LOG);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_SILENT)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_SILENT);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDERRORS);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDMAXERRORS)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BACKGROUNDMAXERRORS);
+ }
+
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL);
+ }
+
+ try {
+ Services.prefs.deleteBranch(PREFBRANCH_APP_UPDATE_NEVER);
+ }
+ catch (e) {
+ }
+}
+
+function setupTimer(aTestTimeout) {
+ gTestTimeout = aTestTimeout;
+ if (gTimeoutTimer) {
+ gTimeoutTimer.cancel();
+ gTimeoutTimer = null;
+ }
+ gTimeoutTimer = Cc["@mozilla.org/timer;1"].
+ createInstance(Ci.nsITimer);
+ gTimeoutTimer.initWithCallback(finishTestTimeout, gTestTimeout,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+}
+
+/**
+ * Closes the update window if it is open and causes the test to fail if an
+ * update window is found.
+ *
+ * @return true if an update window was found, otherwise false.
+ */
+function closeUpdateWindow() {
+ let updateWindow = getUpdateWindow();
+ if (!updateWindow) {
+ return false;
+ }
+
+ ok(false, "Found an existing Update Window from the current or a previous " +
+ "test... attempting to close it.");
+ updateWindow.close();
+ return true;
+}
+
+/**
+ * Gets the update window.
+ *
+ * @return The nsIDOMWindow for the Update Window if it is open and null
+ * if it isn't.
+ */
+function getUpdateWindow() {
+ return Services.wm.getMostRecentWindow(UPDATE_WINDOW_NAME);
+}
+
+/**
+ * Helper for background check errors.
+ */
+const errorsPrefObserver = {
+ observedPref: null,
+ maxErrorPref: null,
+
+ /**
+ * Sets up a preference observer and sets the associated maximum errors
+ * preference used for background notification.
+ *
+ * @param aObservePref
+ * The preference to observe.
+ * @param aMaxErrorPref
+ * The maximum errors preference.
+ * @param aMaxErrorCount
+ * The value to set the maximum errors preference to.
+ */
+ init: function(aObservePref, aMaxErrorPref, aMaxErrorCount) {
+ this.observedPref = aObservePref;
+ this.maxErrorPref = aMaxErrorPref;
+
+ let maxErrors = aMaxErrorCount ? aMaxErrorCount : 2;
+ Services.prefs.setIntPref(aMaxErrorPref, maxErrors);
+ Services.prefs.addObserver(aObservePref, this, false);
+ },
+
+ /**
+ * Preference observer for the preference specified in |this.observedPref|.
+ */
+ observe: function XPI_observe(aSubject, aTopic, aData) {
+ if (aData == this.observedPref) {
+ let errCount = Services.prefs.getIntPref(this.observedPref);
+ let errMax = Services.prefs.getIntPref(this.maxErrorPref);
+ if (errCount >= errMax) {
+ debugDump("removing pref observer");
+ Services.prefs.removeObserver(this.observedPref, this);
+ } else {
+ debugDump("notifying AUS");
+ SimpleTest.executeSoon(function() {
+ gAUS.notify(null);
+ });
+ }
+ }
+ }
+};
diff --git a/toolkit/mozapps/update/tests/data/complete.exe b/toolkit/mozapps/update/tests/data/complete.exe
new file mode 100644
index 000000000..da9cdf0cc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete.exe
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete.mar b/toolkit/mozapps/update/tests/data/complete.mar
new file mode 100644
index 000000000..52306e0a2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete.png b/toolkit/mozapps/update/tests/data/complete.png
new file mode 100644
index 000000000..2990a539f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete.png
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_mac b/toolkit/mozapps/update/tests/data/complete_log_success_mac
new file mode 100644
index 000000000..4f992a137
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_log_success_mac
@@ -0,0 +1,332 @@
+UPDATE TYPE complete
+PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE REMOVEFILE Contents/Resources/removed-files
+PREPARE REMOVEFILE Contents/Resources/precomplete
+PREPARE REMOVEFILE Contents/Resources/2/20/20text0
+PREPARE REMOVEFILE Contents/Resources/2/20/20png0.png
+PREPARE REMOVEFILE Contents/Resources/0/0exe0.exe
+PREPARE REMOVEFILE Contents/Resources/0/00/00text0
+PREPARE REMOVEFILE Contents/MacOS/exe0.exe
+PREPARE REMOVEDIR Contents/Resources/searchplugins/
+PREPARE REMOVEDIR Contents/Resources/defaults/pref/
+PREPARE REMOVEDIR Contents/Resources/defaults/
+PREPARE REMOVEDIR Contents/Resources/2/20/
+PREPARE REMOVEDIR Contents/Resources/2/
+PREPARE REMOVEDIR Contents/Resources/0/00/
+PREPARE REMOVEDIR Contents/Resources/0/
+PREPARE REMOVEDIR Contents/Resources/
+PREPARE REMOVEDIR Contents/MacOS/
+PREPARE REMOVEDIR Contents/
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE ADD Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE ADD Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/removed-files
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE ADD Contents/Resources/1/10/10text0
+PREPARE ADD Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text1
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE ADD Contents/Resources/0/00/00png0.png
+PREPARE ADD Contents/MacOS/exe0.exe
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE REMOVEFILE Contents/Resources/removed-files
+EXECUTE REMOVEFILE Contents/Resources/precomplete
+EXECUTE REMOVEFILE Contents/Resources/2/20/20text0
+EXECUTE REMOVEFILE Contents/Resources/2/20/20png0.png
+EXECUTE REMOVEFILE Contents/Resources/0/0exe0.exe
+EXECUTE REMOVEFILE Contents/Resources/0/00/00text0
+EXECUTE REMOVEFILE Contents/MacOS/exe0.exe
+EXECUTE REMOVEDIR Contents/Resources/searchplugins/
+EXECUTE REMOVEDIR Contents/Resources/defaults/pref/
+EXECUTE REMOVEDIR Contents/Resources/defaults/
+EXECUTE REMOVEDIR Contents/Resources/2/20/
+EXECUTE REMOVEDIR Contents/Resources/2/
+EXECUTE REMOVEDIR Contents/Resources/0/00/
+EXECUTE REMOVEDIR Contents/Resources/0/
+EXECUTE REMOVEDIR Contents/Resources/
+EXECUTE REMOVEDIR Contents/MacOS/
+EXECUTE REMOVEDIR Contents/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/removed-files
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE ADD Contents/Resources/1/10/10text0
+EXECUTE ADD Contents/Resources/0/0exe0.exe
+EXECUTE ADD Contents/Resources/0/00/00text1
+EXECUTE ADD Contents/Resources/0/00/00text0
+EXECUTE ADD Contents/Resources/0/00/00png0.png
+EXECUTE ADD Contents/MacOS/exe0.exe
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/98/
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/970/
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/971/
+EXECUTE REMOVEDIR Contents/Resources/9/97/
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text0
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text1
+EXECUTE REMOVEDIR Contents/Resources/9/96/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/93/
+EXECUTE REMOVEDIR Contents/Resources/9/92/
+EXECUTE REMOVEDIR Contents/Resources/9/91/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/88/
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/870/
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/871/
+EXECUTE REMOVEDIR Contents/Resources/8/87/
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text0
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text1
+EXECUTE REMOVEDIR Contents/Resources/8/86/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/83/
+EXECUTE REMOVEDIR Contents/Resources/8/82/
+EXECUTE REMOVEDIR Contents/Resources/8/81/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/70/
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/71/
+EXECUTE REMOVEFILE Contents/Resources/7/7text0
+EXECUTE REMOVEFILE Contents/Resources/7/7text1
+EXECUTE REMOVEDIR Contents/Resources/7/
+EXECUTE REMOVEDIR Contents/Resources/6/
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+EXECUTE REMOVEFILE Contents/Resources/5/5test.exe
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR Contents/Resources/5/
+EXECUTE REMOVEFILE Contents/Resources/4/4text1
+EXECUTE REMOVEFILE Contents/Resources/4/4text0
+EXECUTE REMOVEDIR Contents/Resources/4/
+EXECUTE REMOVEFILE Contents/Resources/3/3text1
+EXECUTE REMOVEFILE Contents/Resources/3/3text0
+FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH REMOVEFILE Contents/Resources/removed-files
+FINISH REMOVEFILE Contents/Resources/precomplete
+FINISH REMOVEFILE Contents/Resources/2/20/20text0
+FINISH REMOVEFILE Contents/Resources/2/20/20png0.png
+FINISH REMOVEFILE Contents/Resources/0/0exe0.exe
+FINISH REMOVEFILE Contents/Resources/0/00/00text0
+FINISH REMOVEFILE Contents/MacOS/exe0.exe
+FINISH REMOVEDIR Contents/Resources/searchplugins/
+removing directory: Contents/Resources/searchplugins/, rv: 0
+FINISH REMOVEDIR Contents/Resources/defaults/pref/
+removing directory: Contents/Resources/defaults/pref/, rv: 0
+FINISH REMOVEDIR Contents/Resources/defaults/
+removing directory: Contents/Resources/defaults/, rv: 0
+FINISH REMOVEDIR Contents/Resources/2/20/
+FINISH REMOVEDIR Contents/Resources/2/
+FINISH REMOVEDIR Contents/Resources/0/00/
+removing directory: Contents/Resources/0/00/, rv: 0
+FINISH REMOVEDIR Contents/Resources/0/
+removing directory: Contents/Resources/0/, rv: 0
+FINISH REMOVEDIR Contents/Resources/
+removing directory: Contents/Resources/, rv: 0
+FINISH REMOVEDIR Contents/MacOS/
+removing directory: Contents/MacOS/, rv: 0
+FINISH REMOVEDIR Contents/
+removing directory: Contents/, rv: 0
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH ADD Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH ADD Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/removed-files
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH ADD Contents/Resources/1/10/10text0
+FINISH ADD Contents/Resources/0/0exe0.exe
+FINISH ADD Contents/Resources/0/00/00text1
+FINISH ADD Contents/Resources/0/00/00text0
+FINISH ADD Contents/Resources/0/00/00png0.png
+FINISH ADD Contents/MacOS/exe0.exe
+FINISH REMOVEDIR Contents/Resources/9/99/
+FINISH REMOVEDIR Contents/Resources/9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/98/
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/970/
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/971/
+FINISH REMOVEDIR Contents/Resources/9/97/
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+FINISH REMOVEDIR Contents/Resources/9/96/
+FINISH REMOVEDIR Contents/Resources/9/95/
+FINISH REMOVEDIR Contents/Resources/9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/94/
+FINISH REMOVEDIR Contents/Resources/9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/93/
+FINISH REMOVEDIR Contents/Resources/9/92/
+removing directory: Contents/Resources/9/92/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/91/
+removing directory: Contents/Resources/9/91/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/90/
+FINISH REMOVEDIR Contents/Resources/9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/89/
+FINISH REMOVEDIR Contents/Resources/8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/88/
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/870/
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/871/
+FINISH REMOVEDIR Contents/Resources/8/87/
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+FINISH REMOVEDIR Contents/Resources/8/86/
+FINISH REMOVEDIR Contents/Resources/8/85/
+FINISH REMOVEDIR Contents/Resources/8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/84/
+FINISH REMOVEDIR Contents/Resources/8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/83/
+FINISH REMOVEDIR Contents/Resources/8/82/
+removing directory: Contents/Resources/8/82/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/81/
+removing directory: Contents/Resources/8/81/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/80/
+FINISH REMOVEDIR Contents/Resources/8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/70/
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/71/
+FINISH REMOVEFILE Contents/Resources/7/7text0
+FINISH REMOVEFILE Contents/Resources/7/7text1
+FINISH REMOVEDIR Contents/Resources/7/
+FINISH REMOVEDIR Contents/Resources/6/
+FINISH REMOVEFILE Contents/Resources/5/5text1
+FINISH REMOVEFILE Contents/Resources/5/5text0
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+FINISH REMOVEDIR Contents/Resources/5/
+FINISH REMOVEFILE Contents/Resources/4/4text1
+FINISH REMOVEFILE Contents/Resources/4/4text0
+FINISH REMOVEDIR Contents/Resources/4/
+FINISH REMOVEFILE Contents/Resources/3/3text1
+FINISH REMOVEFILE Contents/Resources/3/3text0
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_win b/toolkit/mozapps/update/tests/data/complete_log_success_win
new file mode 100644
index 000000000..c5a03dc9d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_log_success_win
@@ -0,0 +1,320 @@
+UPDATE TYPE complete
+PREPARE REMOVEFILE searchplugins/searchpluginstext0
+PREPARE REMOVEFILE searchplugins/searchpluginspng0.png
+PREPARE REMOVEFILE removed-files
+PREPARE REMOVEFILE precomplete
+PREPARE REMOVEFILE exe0.exe
+PREPARE REMOVEFILE 2/20/20text0
+PREPARE REMOVEFILE 2/20/20png0.png
+PREPARE REMOVEFILE 0/0exe0.exe
+PREPARE REMOVEFILE 0/00/00text0
+PREPARE REMOVEDIR searchplugins/
+PREPARE REMOVEDIR defaults/pref/
+PREPARE REMOVEDIR defaults/
+PREPARE REMOVEDIR 2/20/
+PREPARE REMOVEDIR 2/
+PREPARE REMOVEDIR 0/00/
+PREPARE REMOVEDIR 0/
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE ADD searchplugins/searchpluginspng1.png
+PREPARE ADD searchplugins/searchpluginspng0.png
+PREPARE ADD removed-files
+PREPARE ADD precomplete
+PREPARE ADD exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE ADD distribution/extensions/extensions1/extensions1png1.png
+PREPARE ADD distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE ADD distribution/extensions/extensions0/extensions0png1.png
+PREPARE ADD distribution/extensions/extensions0/extensions0png0.png
+PREPARE ADD 1/10/10text0
+PREPARE ADD 0/0exe0.exe
+PREPARE ADD 0/00/00text1
+PREPARE ADD 0/00/00text0
+PREPARE ADD 0/00/00png0.png
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+EXECUTE REMOVEFILE searchplugins/searchpluginstext0
+EXECUTE REMOVEFILE searchplugins/searchpluginspng0.png
+EXECUTE REMOVEFILE removed-files
+EXECUTE REMOVEFILE precomplete
+EXECUTE REMOVEFILE exe0.exe
+EXECUTE REMOVEFILE 2/20/20text0
+EXECUTE REMOVEFILE 2/20/20png0.png
+EXECUTE REMOVEFILE 0/0exe0.exe
+EXECUTE REMOVEFILE 0/00/00text0
+EXECUTE REMOVEDIR searchplugins/
+EXECUTE REMOVEDIR defaults/pref/
+EXECUTE REMOVEDIR defaults/
+EXECUTE REMOVEDIR 2/20/
+EXECUTE REMOVEDIR 2/
+EXECUTE REMOVEDIR 0/00/
+EXECUTE REMOVEDIR 0/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE ADD searchplugins/searchpluginspng1.png
+EXECUTE ADD searchplugins/searchpluginspng0.png
+EXECUTE ADD removed-files
+EXECUTE ADD precomplete
+EXECUTE ADD exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE ADD distribution/extensions/extensions1/extensions1png1.png
+EXECUTE ADD distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE ADD distribution/extensions/extensions0/extensions0png1.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0png0.png
+EXECUTE ADD 1/10/10text0
+EXECUTE ADD 0/0exe0.exe
+EXECUTE ADD 0/00/00text1
+EXECUTE ADD 0/00/00text0
+EXECUTE ADD 0/00/00png0.png
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/98/
+EXECUTE REMOVEFILE 9/97/970/97xtext0
+EXECUTE REMOVEFILE 9/97/970/97xtext1
+EXECUTE REMOVEDIR 9/97/970/
+EXECUTE REMOVEFILE 9/97/971/97xtext0
+EXECUTE REMOVEFILE 9/97/971/97xtext1
+EXECUTE REMOVEDIR 9/97/971/
+EXECUTE REMOVEDIR 9/97/
+EXECUTE REMOVEFILE 9/96/96text0
+EXECUTE REMOVEFILE 9/96/96text1
+EXECUTE REMOVEDIR 9/96/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/93/
+EXECUTE REMOVEDIR 9/92/
+EXECUTE REMOVEDIR 9/91/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/88/
+EXECUTE REMOVEFILE 8/87/870/87xtext0
+EXECUTE REMOVEFILE 8/87/870/87xtext1
+EXECUTE REMOVEDIR 8/87/870/
+EXECUTE REMOVEFILE 8/87/871/87xtext0
+EXECUTE REMOVEFILE 8/87/871/87xtext1
+EXECUTE REMOVEDIR 8/87/871/
+EXECUTE REMOVEDIR 8/87/
+EXECUTE REMOVEFILE 8/86/86text0
+EXECUTE REMOVEFILE 8/86/86text1
+EXECUTE REMOVEDIR 8/86/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/83/
+EXECUTE REMOVEDIR 8/82/
+EXECUTE REMOVEDIR 8/81/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEFILE 7/70/7xtest.exe
+EXECUTE REMOVEFILE 7/70/7xtext0
+EXECUTE REMOVEFILE 7/70/7xtext1
+EXECUTE REMOVEDIR 7/70/
+EXECUTE REMOVEFILE 7/71/7xtest.exe
+EXECUTE REMOVEFILE 7/71/7xtext0
+EXECUTE REMOVEFILE 7/71/7xtext1
+EXECUTE REMOVEDIR 7/71/
+EXECUTE REMOVEFILE 7/7text0
+EXECUTE REMOVEFILE 7/7text1
+EXECUTE REMOVEDIR 7/
+EXECUTE REMOVEDIR 6/
+EXECUTE REMOVEFILE 5/5text1
+EXECUTE REMOVEFILE 5/5text0
+EXECUTE REMOVEFILE 5/5test.exe
+EXECUTE REMOVEFILE 5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE 5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR 5/
+EXECUTE REMOVEFILE 4/4text1
+EXECUTE REMOVEFILE 4/4text0
+EXECUTE REMOVEDIR 4/
+EXECUTE REMOVEFILE 3/3text1
+EXECUTE REMOVEFILE 3/3text0
+FINISH REMOVEFILE searchplugins/searchpluginstext0
+FINISH REMOVEFILE searchplugins/searchpluginspng0.png
+FINISH REMOVEFILE removed-files
+FINISH REMOVEFILE precomplete
+FINISH REMOVEFILE exe0.exe
+FINISH REMOVEFILE 2/20/20text0
+FINISH REMOVEFILE 2/20/20png0.png
+FINISH REMOVEFILE 0/0exe0.exe
+FINISH REMOVEFILE 0/00/00text0
+FINISH REMOVEDIR searchplugins/
+removing directory: searchplugins/, rv: 0
+FINISH REMOVEDIR defaults/pref/
+removing directory: defaults/pref/, rv: 0
+FINISH REMOVEDIR defaults/
+removing directory: defaults/, rv: 0
+FINISH REMOVEDIR 2/20/
+FINISH REMOVEDIR 2/
+FINISH REMOVEDIR 0/00/
+removing directory: 0/00/, rv: 0
+FINISH REMOVEDIR 0/
+removing directory: 0/, rv: 0
+FINISH ADD searchplugins/searchpluginstext0
+FINISH ADD searchplugins/searchpluginspng1.png
+FINISH ADD searchplugins/searchpluginspng0.png
+FINISH ADD removed-files
+FINISH ADD precomplete
+FINISH ADD exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+FINISH ADD distribution/extensions/extensions1/extensions1png1.png
+FINISH ADD distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH ADD distribution/extensions/extensions0/extensions0png1.png
+FINISH ADD distribution/extensions/extensions0/extensions0png0.png
+FINISH ADD 1/10/10text0
+FINISH ADD 0/0exe0.exe
+FINISH ADD 0/00/00text1
+FINISH ADD 0/00/00text0
+FINISH ADD 0/00/00png0.png
+FINISH REMOVEDIR 9/99/
+FINISH REMOVEDIR 9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/98/
+FINISH REMOVEFILE 9/97/970/97xtext0
+FINISH REMOVEFILE 9/97/970/97xtext1
+FINISH REMOVEDIR 9/97/970/
+FINISH REMOVEFILE 9/97/971/97xtext0
+FINISH REMOVEFILE 9/97/971/97xtext1
+FINISH REMOVEDIR 9/97/971/
+FINISH REMOVEDIR 9/97/
+FINISH REMOVEFILE 9/96/96text0
+FINISH REMOVEFILE 9/96/96text1
+FINISH REMOVEDIR 9/96/
+FINISH REMOVEDIR 9/95/
+FINISH REMOVEDIR 9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/94/
+FINISH REMOVEDIR 9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/93/
+FINISH REMOVEDIR 9/92/
+removing directory: 9/92/, rv: 0
+FINISH REMOVEDIR 9/91/
+removing directory: 9/91/, rv: 0
+FINISH REMOVEDIR 9/90/
+FINISH REMOVEDIR 9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/89/
+FINISH REMOVEDIR 8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/88/
+FINISH REMOVEFILE 8/87/870/87xtext0
+FINISH REMOVEFILE 8/87/870/87xtext1
+FINISH REMOVEDIR 8/87/870/
+FINISH REMOVEFILE 8/87/871/87xtext0
+FINISH REMOVEFILE 8/87/871/87xtext1
+FINISH REMOVEDIR 8/87/871/
+FINISH REMOVEDIR 8/87/
+FINISH REMOVEFILE 8/86/86text0
+FINISH REMOVEFILE 8/86/86text1
+FINISH REMOVEDIR 8/86/
+FINISH REMOVEDIR 8/85/
+FINISH REMOVEDIR 8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/84/
+FINISH REMOVEDIR 8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/83/
+FINISH REMOVEDIR 8/82/
+removing directory: 8/82/, rv: 0
+FINISH REMOVEDIR 8/81/
+removing directory: 8/81/, rv: 0
+FINISH REMOVEDIR 8/80/
+FINISH REMOVEDIR 8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE 7/70/7xtest.exe
+FINISH REMOVEFILE 7/70/7xtext0
+FINISH REMOVEFILE 7/70/7xtext1
+FINISH REMOVEDIR 7/70/
+FINISH REMOVEFILE 7/71/7xtest.exe
+FINISH REMOVEFILE 7/71/7xtext0
+FINISH REMOVEFILE 7/71/7xtext1
+FINISH REMOVEDIR 7/71/
+FINISH REMOVEFILE 7/7text0
+FINISH REMOVEFILE 7/7text1
+FINISH REMOVEDIR 7/
+FINISH REMOVEDIR 6/
+FINISH REMOVEFILE 5/5text1
+FINISH REMOVEFILE 5/5text0
+FINISH REMOVEFILE 5/5test.exe
+FINISH REMOVEDIR 5/
+FINISH REMOVEFILE 4/4text1
+FINISH REMOVEFILE 4/4text0
+FINISH REMOVEDIR 4/
+FINISH REMOVEFILE 3/3text1
+FINISH REMOVEFILE 3/3text0
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/complete_mac.mar b/toolkit/mozapps/update/tests/data/complete_mac.mar
new file mode 100644
index 000000000..ca1497f4f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_mac.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete b/toolkit/mozapps/update/tests/data/complete_precomplete
new file mode 100644
index 000000000..ae7a0013f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_precomplete
@@ -0,0 +1,18 @@
+remove "searchplugins/searchpluginstext0"
+remove "searchplugins/searchpluginspng1.png"
+remove "searchplugins/searchpluginspng0.png"
+remove "removed-files"
+remove "precomplete"
+remove "exe0.exe"
+remove "1/10/10text0"
+remove "0/0exe0.exe"
+remove "0/00/00text1"
+remove "0/00/00text0"
+remove "0/00/00png0.png"
+rmdir "searchplugins/"
+rmdir "defaults/pref/"
+rmdir "defaults/"
+rmdir "1/10/"
+rmdir "1/"
+rmdir "0/00/"
+rmdir "0/"
diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete_mac b/toolkit/mozapps/update/tests/data/complete_precomplete_mac
new file mode 100644
index 000000000..8d81a36d6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_precomplete_mac
@@ -0,0 +1,21 @@
+remove "Contents/Resources/searchplugins/searchpluginstext0"
+remove "Contents/Resources/searchplugins/searchpluginspng1.png"
+remove "Contents/Resources/searchplugins/searchpluginspng0.png"
+remove "Contents/Resources/removed-files"
+remove "Contents/Resources/precomplete"
+remove "Contents/Resources/1/10/10text0"
+remove "Contents/Resources/0/0exe0.exe"
+remove "Contents/Resources/0/00/00text1"
+remove "Contents/Resources/0/00/00text0"
+remove "Contents/Resources/0/00/00png0.png"
+remove "Contents/MacOS/exe0.exe"
+rmdir "Contents/Resources/searchplugins/"
+rmdir "Contents/Resources/defaults/pref/"
+rmdir "Contents/Resources/defaults/"
+rmdir "Contents/Resources/1/10/"
+rmdir "Contents/Resources/1/"
+rmdir "Contents/Resources/0/00/"
+rmdir "Contents/Resources/0/"
+rmdir "Contents/Resources/"
+rmdir "Contents/MacOS/"
+rmdir "Contents/"
diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files b/toolkit/mozapps/update/tests/data/complete_removed-files
new file mode 100644
index 000000000..e45c43c1f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_removed-files
@@ -0,0 +1,41 @@
+text0
+text1
+3/3text0
+3/3text1
+4/exe0.exe
+4/4text0
+4/4text1
+4/
+5/5text0
+5/5text1
+5/*
+6/
+7/*
+8/80/
+8/81/
+8/82/
+8/83/
+8/84/
+8/85/*
+8/86/*
+8/87/*
+8/88/*
+8/89/*
+8/80/
+8/84/*
+8/85/*
+8/89/
+9/90/
+9/91/
+9/92/
+9/93/
+9/94/
+9/95/*
+9/96/*
+9/97/*
+9/98/*
+9/99/*
+9/90/
+9/94/*
+9/95/*
+9/99/
diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files_mac b/toolkit/mozapps/update/tests/data/complete_removed-files_mac
new file mode 100644
index 000000000..955dc5b34
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_removed-files_mac
@@ -0,0 +1,41 @@
+Contents/Resources/text0
+Contents/Resources/text1
+Contents/Resources/3/3text0
+Contents/Resources/3/3text1
+Contents/Resources/4/exe0.exe
+Contents/Resources/4/4text0
+Contents/Resources/4/4text1
+Contents/Resources/4/
+Contents/Resources/5/5text0
+Contents/Resources/5/5text1
+Contents/Resources/5/*
+Contents/Resources/6/
+Contents/Resources/7/*
+Contents/Resources/8/80/
+Contents/Resources/8/81/
+Contents/Resources/8/82/
+Contents/Resources/8/83/
+Contents/Resources/8/84/
+Contents/Resources/8/85/*
+Contents/Resources/8/86/*
+Contents/Resources/8/87/*
+Contents/Resources/8/88/*
+Contents/Resources/8/89/*
+Contents/Resources/8/80/
+Contents/Resources/8/84/*
+Contents/Resources/8/85/*
+Contents/Resources/8/89/
+Contents/Resources/9/90/
+Contents/Resources/9/91/
+Contents/Resources/9/92/
+Contents/Resources/9/93/
+Contents/Resources/9/94/
+Contents/Resources/9/95/*
+Contents/Resources/9/96/*
+Contents/Resources/9/97/*
+Contents/Resources/9/98/*
+Contents/Resources/9/99/*
+Contents/Resources/9/90/
+Contents/Resources/9/94/*
+Contents/Resources/9/95/*
+Contents/Resources/9/99/
diff --git a/toolkit/mozapps/update/tests/data/complete_update_manifest b/toolkit/mozapps/update/tests/data/complete_update_manifest
new file mode 100644
index 000000000..383a324f6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_update_manifest
@@ -0,0 +1,59 @@
+type "complete"
+add "precomplete"
+add "searchplugins/searchpluginstext0"
+add "searchplugins/searchpluginspng1.png"
+add "searchplugins/searchpluginspng0.png"
+add "removed-files"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1text0"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1png1.png"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1png0.png"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0text0"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0png1.png"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0png0.png"
+add "exe0.exe"
+add "1/10/10text0"
+add "0/0exe0.exe"
+add "0/00/00text1"
+add "0/00/00text0"
+add "0/00/00png0.png"
+remove "text1"
+remove "text0"
+rmrfdir "9/99/"
+rmdir "9/99/"
+rmrfdir "9/98/"
+rmrfdir "9/97/"
+rmrfdir "9/96/"
+rmrfdir "9/95/"
+rmrfdir "9/95/"
+rmrfdir "9/94/"
+rmdir "9/94/"
+rmdir "9/93/"
+rmdir "9/92/"
+rmdir "9/91/"
+rmdir "9/90/"
+rmdir "9/90/"
+rmrfdir "8/89/"
+rmdir "8/89/"
+rmrfdir "8/88/"
+rmrfdir "8/87/"
+rmrfdir "8/86/"
+rmrfdir "8/85/"
+rmrfdir "8/85/"
+rmrfdir "8/84/"
+rmdir "8/84/"
+rmdir "8/83/"
+rmdir "8/82/"
+rmdir "8/81/"
+rmdir "8/80/"
+rmdir "8/80/"
+rmrfdir "7/"
+rmdir "6/"
+remove "5/5text1"
+remove "5/5text0"
+rmrfdir "5/"
+remove "4/exe0.exe"
+remove "4/4text1"
+remove "4/4text0"
+rmdir "4/"
+remove "3/3text1"
+remove "3/3text0"
diff --git a/toolkit/mozapps/update/tests/data/old_version.mar b/toolkit/mozapps/update/tests/data/old_version.mar
new file mode 100644
index 000000000..31550698a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/old_version.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial.exe b/toolkit/mozapps/update/tests/data/partial.exe
new file mode 100644
index 000000000..3949fd2a0
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial.exe
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial.mar b/toolkit/mozapps/update/tests/data/partial.mar
new file mode 100644
index 000000000..789d3d98d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial.png b/toolkit/mozapps/update/tests/data/partial.png
new file mode 100644
index 000000000..9246f586c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial.png
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_mac b/toolkit/mozapps/update/tests/data/partial_log_failure_mac
new file mode 100644
index 000000000..3b2933ebd
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_failure_mac
@@ -0,0 +1,192 @@
+UPDATE TYPE partial
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE PATCH Contents/Resources/0/00/00png0.png
+PREPARE PATCH Contents/MacOS/exe0.exe
+PREPARE ADD Contents/Resources/2/20/20text0
+PREPARE ADD Contents/Resources/2/20/20png0.png
+PREPARE ADD Contents/Resources/0/00/00text2
+PREPARE REMOVEFILE Contents/Resources/1/10/10text0
+PREPARE REMOVEFILE Contents/Resources/0/00/00text1
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+PREPARE REMOVEDIR Contents/Resources/1/10/
+PREPARE REMOVEDIR Contents/Resources/1/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH Contents/Resources/0/0exe0.exe
+LoadSourceFile: destination file size 776 does not match expected size 79872
+LoadSourceFile failed
+### execution failed
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+backup_restore: backup file doesn't exist: Contents/Resources/distribution/extensions/extensions1/extensions1text0.moz-backup
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH Contents/Resources/0/0exe0.exe
+backup_restore: backup file doesn't exist: Contents/Resources/0/0exe0.exe.moz-backup
+FINISH ADD Contents/Resources/0/00/00text0
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text0.moz-backup
+FINISH PATCH Contents/Resources/0/00/00png0.png
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00png0.png.moz-backup
+FINISH PATCH Contents/MacOS/exe0.exe
+backup_restore: backup file doesn't exist: Contents/MacOS/exe0.exe.moz-backup
+FINISH ADD Contents/Resources/2/20/20text0
+backup_restore: backup file doesn't exist: Contents/Resources/2/20/20text0.moz-backup
+FINISH ADD Contents/Resources/2/20/20png0.png
+backup_restore: backup file doesn't exist: Contents/Resources/2/20/20png0.png.moz-backup
+FINISH ADD Contents/Resources/0/00/00text2
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text2.moz-backup
+FINISH REMOVEFILE Contents/Resources/1/10/10text0
+backup_restore: backup file doesn't exist: Contents/Resources/1/10/10text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/0/00/00text1
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtest.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtest.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/7text0
+backup_restore: backup file doesn't exist: Contents/Resources/7/7text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/7text1
+backup_restore: backup file doesn't exist: Contents/Resources/7/7text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text1
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text0
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+backup_restore: backup file doesn't exist: Contents/Resources/5/5test.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text0
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text1
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/4/4text1
+backup_restore: backup file doesn't exist: Contents/Resources/4/4text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/4/4text0
+backup_restore: backup file doesn't exist: Contents/Resources/4/4text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/3/3text1
+backup_restore: backup file doesn't exist: Contents/Resources/3/3text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/3/3text0
+backup_restore: backup file doesn't exist: Contents/Resources/3/3text0.moz-backup
+failed: 2
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_win b/toolkit/mozapps/update/tests/data/partial_log_failure_win
new file mode 100644
index 000000000..e3d683dc1
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_failure_win
@@ -0,0 +1,192 @@
+UPDATE TYPE partial
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE PATCH searchplugins/searchpluginspng1.png
+PREPARE PATCH searchplugins/searchpluginspng0.png
+PREPARE ADD precomplete
+PREPARE PATCH exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH 0/0exe0.exe
+PREPARE ADD 0/00/00text0
+PREPARE PATCH 0/00/00png0.png
+PREPARE ADD 2/20/20text0
+PREPARE ADD 2/20/20png0.png
+PREPARE ADD 0/00/00text2
+PREPARE REMOVEFILE 1/10/10text0
+PREPARE REMOVEFILE 0/00/00text1
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+PREPARE REMOVEDIR 1/10/
+PREPARE REMOVEDIR 1/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE PATCH searchplugins/searchpluginspng1.png
+EXECUTE PATCH searchplugins/searchpluginspng0.png
+EXECUTE ADD precomplete
+EXECUTE PATCH exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH 0/0exe0.exe
+LoadSourceFile: destination file size 776 does not match expected size 79872
+LoadSourceFile failed
+### execution failed
+FINISH ADD searchplugins/searchpluginstext0
+FINISH PATCH searchplugins/searchpluginspng1.png
+FINISH PATCH searchplugins/searchpluginspng0.png
+FINISH ADD precomplete
+FINISH PATCH exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+backup_restore: backup file doesn't exist: distribution/extensions/extensions1/extensions1text0.moz-backup
+FINISH PATCH distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH PATCH distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH 0/0exe0.exe
+backup_restore: backup file doesn't exist: 0/0exe0.exe.moz-backup
+FINISH ADD 0/00/00text0
+backup_restore: backup file doesn't exist: 0/00/00text0.moz-backup
+FINISH PATCH 0/00/00png0.png
+backup_restore: backup file doesn't exist: 0/00/00png0.png.moz-backup
+FINISH ADD 2/20/20text0
+backup_restore: backup file doesn't exist: 2/20/20text0.moz-backup
+FINISH ADD 2/20/20png0.png
+backup_restore: backup file doesn't exist: 2/20/20png0.png.moz-backup
+FINISH ADD 0/00/00text2
+backup_restore: backup file doesn't exist: 0/00/00text2.moz-backup
+FINISH REMOVEFILE 1/10/10text0
+backup_restore: backup file doesn't exist: 1/10/10text0.moz-backup
+FINISH REMOVEFILE 0/00/00text1
+backup_restore: backup file doesn't exist: 0/00/00text1.moz-backup
+FINISH REMOVEFILE 9/97/970/97xtext0
+backup_restore: backup file doesn't exist: 9/97/970/97xtext0.moz-backup
+FINISH REMOVEFILE 9/97/970/97xtext1
+backup_restore: backup file doesn't exist: 9/97/970/97xtext1.moz-backup
+FINISH REMOVEFILE 9/97/971/97xtext0
+backup_restore: backup file doesn't exist: 9/97/971/97xtext0.moz-backup
+FINISH REMOVEFILE 9/97/971/97xtext1
+backup_restore: backup file doesn't exist: 9/97/971/97xtext1.moz-backup
+FINISH REMOVEFILE 9/96/96text0
+backup_restore: backup file doesn't exist: 9/96/96text0.moz-backup
+FINISH REMOVEFILE 9/96/96text1
+backup_restore: backup file doesn't exist: 9/96/96text1.moz-backup
+FINISH REMOVEFILE 8/87/870/87xtext0
+backup_restore: backup file doesn't exist: 8/87/870/87xtext0.moz-backup
+FINISH REMOVEFILE 8/87/870/87xtext1
+backup_restore: backup file doesn't exist: 8/87/870/87xtext1.moz-backup
+FINISH REMOVEFILE 8/87/871/87xtext0
+backup_restore: backup file doesn't exist: 8/87/871/87xtext0.moz-backup
+FINISH REMOVEFILE 8/87/871/87xtext1
+backup_restore: backup file doesn't exist: 8/87/871/87xtext1.moz-backup
+FINISH REMOVEFILE 8/86/86text0
+backup_restore: backup file doesn't exist: 8/86/86text0.moz-backup
+FINISH REMOVEFILE 8/86/86text1
+backup_restore: backup file doesn't exist: 8/86/86text1.moz-backup
+FINISH REMOVEFILE 7/70/7xtest.exe
+backup_restore: backup file doesn't exist: 7/70/7xtest.exe.moz-backup
+FINISH REMOVEFILE 7/70/7xtext0
+backup_restore: backup file doesn't exist: 7/70/7xtext0.moz-backup
+FINISH REMOVEFILE 7/70/7xtext1
+backup_restore: backup file doesn't exist: 7/70/7xtext1.moz-backup
+FINISH REMOVEFILE 7/71/7xtest.exe
+backup_restore: backup file doesn't exist: 7/71/7xtest.exe.moz-backup
+FINISH REMOVEFILE 7/71/7xtext0
+backup_restore: backup file doesn't exist: 7/71/7xtext0.moz-backup
+FINISH REMOVEFILE 7/71/7xtext1
+backup_restore: backup file doesn't exist: 7/71/7xtext1.moz-backup
+FINISH REMOVEFILE 7/7text0
+backup_restore: backup file doesn't exist: 7/7text0.moz-backup
+FINISH REMOVEFILE 7/7text1
+backup_restore: backup file doesn't exist: 7/7text1.moz-backup
+FINISH REMOVEFILE 5/5text1
+backup_restore: backup file doesn't exist: 5/5text1.moz-backup
+FINISH REMOVEFILE 5/5text0
+backup_restore: backup file doesn't exist: 5/5text0.moz-backup
+FINISH REMOVEFILE 5/5test.exe
+backup_restore: backup file doesn't exist: 5/5test.exe.moz-backup
+FINISH REMOVEFILE 5/5text0
+backup_restore: backup file doesn't exist: 5/5text0.moz-backup
+FINISH REMOVEFILE 5/5text1
+backup_restore: backup file doesn't exist: 5/5text1.moz-backup
+FINISH REMOVEFILE 4/4text1
+backup_restore: backup file doesn't exist: 4/4text1.moz-backup
+FINISH REMOVEFILE 4/4text0
+backup_restore: backup file doesn't exist: 4/4text0.moz-backup
+FINISH REMOVEFILE 3/3text1
+backup_restore: backup file doesn't exist: 3/3text1.moz-backup
+FINISH REMOVEFILE 3/3text0
+backup_restore: backup file doesn't exist: 3/3text0.moz-backup
+failed: 2
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_mac b/toolkit/mozapps/update/tests/data/partial_log_success_mac
new file mode 100644
index 000000000..fb5272ad2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_success_mac
@@ -0,0 +1,279 @@
+UPDATE TYPE partial
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE PATCH Contents/Resources/0/00/00png0.png
+PREPARE PATCH Contents/MacOS/exe0.exe
+PREPARE ADD Contents/Resources/2/20/20text0
+PREPARE ADD Contents/Resources/2/20/20png0.png
+PREPARE ADD Contents/Resources/0/00/00text2
+PREPARE REMOVEFILE Contents/Resources/1/10/10text0
+PREPARE REMOVEFILE Contents/Resources/0/00/00text1
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+PREPARE REMOVEDIR Contents/Resources/1/10/
+PREPARE REMOVEDIR Contents/Resources/1/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH Contents/Resources/0/0exe0.exe
+EXECUTE ADD Contents/Resources/0/00/00text0
+EXECUTE PATCH Contents/Resources/0/00/00png0.png
+EXECUTE PATCH Contents/MacOS/exe0.exe
+EXECUTE ADD Contents/Resources/2/20/20text0
+EXECUTE ADD Contents/Resources/2/20/20png0.png
+EXECUTE ADD Contents/Resources/0/00/00text2
+EXECUTE REMOVEFILE Contents/Resources/1/10/10text0
+EXECUTE REMOVEFILE Contents/Resources/0/00/00text1
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/98/
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/970/
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/971/
+EXECUTE REMOVEDIR Contents/Resources/9/97/
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text0
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text1
+EXECUTE REMOVEDIR Contents/Resources/9/96/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/93/
+EXECUTE REMOVEDIR Contents/Resources/9/92/
+EXECUTE REMOVEDIR Contents/Resources/9/91/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/88/
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/870/
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/871/
+EXECUTE REMOVEDIR Contents/Resources/8/87/
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text0
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text1
+EXECUTE REMOVEDIR Contents/Resources/8/86/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/83/
+EXECUTE REMOVEDIR Contents/Resources/8/82/
+EXECUTE REMOVEDIR Contents/Resources/8/81/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/70/
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/71/
+EXECUTE REMOVEFILE Contents/Resources/7/7text0
+EXECUTE REMOVEFILE Contents/Resources/7/7text1
+EXECUTE REMOVEDIR Contents/Resources/7/
+EXECUTE REMOVEDIR Contents/Resources/6/
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+EXECUTE REMOVEFILE Contents/Resources/5/5test.exe
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR Contents/Resources/5/
+EXECUTE REMOVEFILE Contents/Resources/4/4text1
+EXECUTE REMOVEFILE Contents/Resources/4/4text0
+EXECUTE REMOVEDIR Contents/Resources/4/
+EXECUTE REMOVEFILE Contents/Resources/3/3text1
+EXECUTE REMOVEFILE Contents/Resources/3/3text0
+EXECUTE REMOVEDIR Contents/Resources/1/10/
+EXECUTE REMOVEDIR Contents/Resources/1/
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH Contents/Resources/0/0exe0.exe
+FINISH ADD Contents/Resources/0/00/00text0
+FINISH PATCH Contents/Resources/0/00/00png0.png
+FINISH PATCH Contents/MacOS/exe0.exe
+FINISH ADD Contents/Resources/2/20/20text0
+FINISH ADD Contents/Resources/2/20/20png0.png
+FINISH ADD Contents/Resources/0/00/00text2
+FINISH REMOVEFILE Contents/Resources/1/10/10text0
+FINISH REMOVEFILE Contents/Resources/0/00/00text1
+FINISH REMOVEDIR Contents/Resources/9/99/
+FINISH REMOVEDIR Contents/Resources/9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/98/
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/970/
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/971/
+FINISH REMOVEDIR Contents/Resources/9/97/
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+FINISH REMOVEDIR Contents/Resources/9/96/
+FINISH REMOVEDIR Contents/Resources/9/95/
+FINISH REMOVEDIR Contents/Resources/9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/94/
+FINISH REMOVEDIR Contents/Resources/9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/93/
+FINISH REMOVEDIR Contents/Resources/9/92/
+removing directory: Contents/Resources/9/92/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/91/
+removing directory: Contents/Resources/9/91/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/90/
+FINISH REMOVEDIR Contents/Resources/9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/89/
+FINISH REMOVEDIR Contents/Resources/8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/88/
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/870/
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/871/
+FINISH REMOVEDIR Contents/Resources/8/87/
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+FINISH REMOVEDIR Contents/Resources/8/86/
+FINISH REMOVEDIR Contents/Resources/8/85/
+FINISH REMOVEDIR Contents/Resources/8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/84/
+FINISH REMOVEDIR Contents/Resources/8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/83/
+FINISH REMOVEDIR Contents/Resources/8/82/
+removing directory: Contents/Resources/8/82/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/81/
+removing directory: Contents/Resources/8/81/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/80/
+FINISH REMOVEDIR Contents/Resources/8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/70/
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/71/
+FINISH REMOVEFILE Contents/Resources/7/7text0
+FINISH REMOVEFILE Contents/Resources/7/7text1
+FINISH REMOVEDIR Contents/Resources/7/
+FINISH REMOVEDIR Contents/Resources/6/
+FINISH REMOVEFILE Contents/Resources/5/5text1
+FINISH REMOVEFILE Contents/Resources/5/5text0
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+FINISH REMOVEDIR Contents/Resources/5/
+FINISH REMOVEFILE Contents/Resources/4/4text1
+FINISH REMOVEFILE Contents/Resources/4/4text0
+FINISH REMOVEDIR Contents/Resources/4/
+FINISH REMOVEFILE Contents/Resources/3/3text1
+FINISH REMOVEFILE Contents/Resources/3/3text0
+FINISH REMOVEDIR Contents/Resources/1/10/
+FINISH REMOVEDIR Contents/Resources/1/
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_win b/toolkit/mozapps/update/tests/data/partial_log_success_win
new file mode 100644
index 000000000..1f5c4b3b4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_success_win
@@ -0,0 +1,279 @@
+UPDATE TYPE partial
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE PATCH searchplugins/searchpluginspng1.png
+PREPARE PATCH searchplugins/searchpluginspng0.png
+PREPARE ADD precomplete
+PREPARE PATCH exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH 0/0exe0.exe
+PREPARE ADD 0/00/00text0
+PREPARE PATCH 0/00/00png0.png
+PREPARE ADD 2/20/20text0
+PREPARE ADD 2/20/20png0.png
+PREPARE ADD 0/00/00text2
+PREPARE REMOVEFILE 1/10/10text0
+PREPARE REMOVEFILE 0/00/00text1
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+PREPARE REMOVEDIR 1/10/
+PREPARE REMOVEDIR 1/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE PATCH searchplugins/searchpluginspng1.png
+EXECUTE PATCH searchplugins/searchpluginspng0.png
+EXECUTE ADD precomplete
+EXECUTE PATCH exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH 0/0exe0.exe
+EXECUTE ADD 0/00/00text0
+EXECUTE PATCH 0/00/00png0.png
+EXECUTE ADD 2/20/20text0
+EXECUTE ADD 2/20/20png0.png
+EXECUTE ADD 0/00/00text2
+EXECUTE REMOVEFILE 1/10/10text0
+EXECUTE REMOVEFILE 0/00/00text1
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/98/
+EXECUTE REMOVEFILE 9/97/970/97xtext0
+EXECUTE REMOVEFILE 9/97/970/97xtext1
+EXECUTE REMOVEDIR 9/97/970/
+EXECUTE REMOVEFILE 9/97/971/97xtext0
+EXECUTE REMOVEFILE 9/97/971/97xtext1
+EXECUTE REMOVEDIR 9/97/971/
+EXECUTE REMOVEDIR 9/97/
+EXECUTE REMOVEFILE 9/96/96text0
+EXECUTE REMOVEFILE 9/96/96text1
+EXECUTE REMOVEDIR 9/96/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/93/
+EXECUTE REMOVEDIR 9/92/
+EXECUTE REMOVEDIR 9/91/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/88/
+EXECUTE REMOVEFILE 8/87/870/87xtext0
+EXECUTE REMOVEFILE 8/87/870/87xtext1
+EXECUTE REMOVEDIR 8/87/870/
+EXECUTE REMOVEFILE 8/87/871/87xtext0
+EXECUTE REMOVEFILE 8/87/871/87xtext1
+EXECUTE REMOVEDIR 8/87/871/
+EXECUTE REMOVEDIR 8/87/
+EXECUTE REMOVEFILE 8/86/86text0
+EXECUTE REMOVEFILE 8/86/86text1
+EXECUTE REMOVEDIR 8/86/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/83/
+EXECUTE REMOVEDIR 8/82/
+EXECUTE REMOVEDIR 8/81/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEFILE 7/70/7xtest.exe
+EXECUTE REMOVEFILE 7/70/7xtext0
+EXECUTE REMOVEFILE 7/70/7xtext1
+EXECUTE REMOVEDIR 7/70/
+EXECUTE REMOVEFILE 7/71/7xtest.exe
+EXECUTE REMOVEFILE 7/71/7xtext0
+EXECUTE REMOVEFILE 7/71/7xtext1
+EXECUTE REMOVEDIR 7/71/
+EXECUTE REMOVEFILE 7/7text0
+EXECUTE REMOVEFILE 7/7text1
+EXECUTE REMOVEDIR 7/
+EXECUTE REMOVEDIR 6/
+EXECUTE REMOVEFILE 5/5text1
+EXECUTE REMOVEFILE 5/5text0
+EXECUTE REMOVEFILE 5/5test.exe
+EXECUTE REMOVEFILE 5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE 5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR 5/
+EXECUTE REMOVEFILE 4/4text1
+EXECUTE REMOVEFILE 4/4text0
+EXECUTE REMOVEDIR 4/
+EXECUTE REMOVEFILE 3/3text1
+EXECUTE REMOVEFILE 3/3text0
+EXECUTE REMOVEDIR 1/10/
+EXECUTE REMOVEDIR 1/
+FINISH ADD searchplugins/searchpluginstext0
+FINISH PATCH searchplugins/searchpluginspng1.png
+FINISH PATCH searchplugins/searchpluginspng0.png
+FINISH ADD precomplete
+FINISH PATCH exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+FINISH PATCH distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH PATCH distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH 0/0exe0.exe
+FINISH ADD 0/00/00text0
+FINISH PATCH 0/00/00png0.png
+FINISH ADD 2/20/20text0
+FINISH ADD 2/20/20png0.png
+FINISH ADD 0/00/00text2
+FINISH REMOVEFILE 1/10/10text0
+FINISH REMOVEFILE 0/00/00text1
+FINISH REMOVEDIR 9/99/
+FINISH REMOVEDIR 9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/98/
+FINISH REMOVEFILE 9/97/970/97xtext0
+FINISH REMOVEFILE 9/97/970/97xtext1
+FINISH REMOVEDIR 9/97/970/
+FINISH REMOVEFILE 9/97/971/97xtext0
+FINISH REMOVEFILE 9/97/971/97xtext1
+FINISH REMOVEDIR 9/97/971/
+FINISH REMOVEDIR 9/97/
+FINISH REMOVEFILE 9/96/96text0
+FINISH REMOVEFILE 9/96/96text1
+FINISH REMOVEDIR 9/96/
+FINISH REMOVEDIR 9/95/
+FINISH REMOVEDIR 9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/94/
+FINISH REMOVEDIR 9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/93/
+FINISH REMOVEDIR 9/92/
+removing directory: 9/92/, rv: 0
+FINISH REMOVEDIR 9/91/
+removing directory: 9/91/, rv: 0
+FINISH REMOVEDIR 9/90/
+FINISH REMOVEDIR 9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/89/
+FINISH REMOVEDIR 8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/88/
+FINISH REMOVEFILE 8/87/870/87xtext0
+FINISH REMOVEFILE 8/87/870/87xtext1
+FINISH REMOVEDIR 8/87/870/
+FINISH REMOVEFILE 8/87/871/87xtext0
+FINISH REMOVEFILE 8/87/871/87xtext1
+FINISH REMOVEDIR 8/87/871/
+FINISH REMOVEDIR 8/87/
+FINISH REMOVEFILE 8/86/86text0
+FINISH REMOVEFILE 8/86/86text1
+FINISH REMOVEDIR 8/86/
+FINISH REMOVEDIR 8/85/
+FINISH REMOVEDIR 8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/84/
+FINISH REMOVEDIR 8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/83/
+FINISH REMOVEDIR 8/82/
+removing directory: 8/82/, rv: 0
+FINISH REMOVEDIR 8/81/
+removing directory: 8/81/, rv: 0
+FINISH REMOVEDIR 8/80/
+FINISH REMOVEDIR 8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE 7/70/7xtest.exe
+FINISH REMOVEFILE 7/70/7xtext0
+FINISH REMOVEFILE 7/70/7xtext1
+FINISH REMOVEDIR 7/70/
+FINISH REMOVEFILE 7/71/7xtest.exe
+FINISH REMOVEFILE 7/71/7xtext0
+FINISH REMOVEFILE 7/71/7xtext1
+FINISH REMOVEDIR 7/71/
+FINISH REMOVEFILE 7/7text0
+FINISH REMOVEFILE 7/7text1
+FINISH REMOVEDIR 7/
+FINISH REMOVEDIR 6/
+FINISH REMOVEFILE 5/5text1
+FINISH REMOVEFILE 5/5text0
+FINISH REMOVEFILE 5/5test.exe
+FINISH REMOVEDIR 5/
+FINISH REMOVEFILE 4/4text1
+FINISH REMOVEFILE 4/4text0
+FINISH REMOVEDIR 4/
+FINISH REMOVEFILE 3/3text1
+FINISH REMOVEFILE 3/3text0
+FINISH REMOVEDIR 1/10/
+FINISH REMOVEDIR 1/
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_mac.mar b/toolkit/mozapps/update/tests/data/partial_mac.mar
new file mode 100644
index 000000000..5a702ed4a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_mac.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete b/toolkit/mozapps/update/tests/data/partial_precomplete
new file mode 100644
index 000000000..3ec201463
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_precomplete
@@ -0,0 +1,19 @@
+remove "searchplugins/searchpluginstext0"
+remove "searchplugins/searchpluginspng1.png"
+remove "searchplugins/searchpluginspng0.png"
+remove "removed-files"
+remove "precomplete"
+remove "exe0.exe"
+remove "2/20/20text0"
+remove "2/20/20png0.png"
+remove "0/0exe0.exe"
+remove "0/00/00text2"
+remove "0/00/00text0"
+remove "0/00/00png0.png"
+rmdir "searchplugins/"
+rmdir "defaults/pref/"
+rmdir "defaults/"
+rmdir "2/20/"
+rmdir "2/"
+rmdir "0/00/"
+rmdir "0/"
diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete_mac b/toolkit/mozapps/update/tests/data/partial_precomplete_mac
new file mode 100644
index 000000000..c65b6e4e3
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_precomplete_mac
@@ -0,0 +1,22 @@
+remove "Contents/Resources/searchplugins/searchpluginstext0"
+remove "Contents/Resources/searchplugins/searchpluginspng1.png"
+remove "Contents/Resources/searchplugins/searchpluginspng0.png"
+remove "Contents/Resources/removed-files"
+remove "Contents/Resources/precomplete"
+remove "Contents/Resources/2/20/20text0"
+remove "Contents/Resources/2/20/20png0.png"
+remove "Contents/Resources/0/0exe0.exe"
+remove "Contents/Resources/0/00/00text2"
+remove "Contents/Resources/0/00/00text0"
+remove "Contents/Resources/0/00/00png0.png"
+remove "Contents/MacOS/exe0.exe"
+rmdir "Contents/Resources/searchplugins/"
+rmdir "Contents/Resources/defaults/pref/"
+rmdir "Contents/Resources/defaults/"
+rmdir "Contents/Resources/2/20/"
+rmdir "Contents/Resources/2/"
+rmdir "Contents/Resources/0/00/"
+rmdir "Contents/Resources/0/"
+rmdir "Contents/Resources/"
+rmdir "Contents/MacOS/"
+rmdir "Contents/"
diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files b/toolkit/mozapps/update/tests/data/partial_removed-files
new file mode 100644
index 000000000..881311b82
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_removed-files
@@ -0,0 +1,41 @@
+a/b/text0
+a/b/text1
+a/b/3/3text0
+a/b/3/3text1
+a/b/4/4exe0.exe
+a/b/4/4text0
+a/b/4/4text1
+a/b/4/
+a/b/5/5text0
+a/b/5/5text1
+a/b/5/*
+a/b/6/
+a/b/7/*
+a/b/8/80/
+a/b/8/81/
+a/b/8/82/
+a/b/8/83/
+a/b/8/84/
+a/b/8/85/*
+a/b/8/86/*
+a/b/8/87/*
+a/b/8/88/*
+a/b/8/89/*
+a/b/8/80/
+a/b/8/84/*
+a/b/8/85/*
+a/b/8/89/
+a/b/9/90/
+a/b/9/91/
+a/b/9/92/
+a/b/9/93/
+a/b/9/94/
+a/b/9/95/*
+a/b/9/96/*
+a/b/9/97/*
+a/b/9/98/*
+a/b/9/99/*
+a/b/9/90/
+a/b/9/94/*
+a/b/9/95/*
+a/b/9/99/
diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files_mac b/toolkit/mozapps/update/tests/data/partial_removed-files_mac
new file mode 100644
index 000000000..955dc5b34
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_removed-files_mac
@@ -0,0 +1,41 @@
+Contents/Resources/text0
+Contents/Resources/text1
+Contents/Resources/3/3text0
+Contents/Resources/3/3text1
+Contents/Resources/4/exe0.exe
+Contents/Resources/4/4text0
+Contents/Resources/4/4text1
+Contents/Resources/4/
+Contents/Resources/5/5text0
+Contents/Resources/5/5text1
+Contents/Resources/5/*
+Contents/Resources/6/
+Contents/Resources/7/*
+Contents/Resources/8/80/
+Contents/Resources/8/81/
+Contents/Resources/8/82/
+Contents/Resources/8/83/
+Contents/Resources/8/84/
+Contents/Resources/8/85/*
+Contents/Resources/8/86/*
+Contents/Resources/8/87/*
+Contents/Resources/8/88/*
+Contents/Resources/8/89/*
+Contents/Resources/8/80/
+Contents/Resources/8/84/*
+Contents/Resources/8/85/*
+Contents/Resources/8/89/
+Contents/Resources/9/90/
+Contents/Resources/9/91/
+Contents/Resources/9/92/
+Contents/Resources/9/93/
+Contents/Resources/9/94/
+Contents/Resources/9/95/*
+Contents/Resources/9/96/*
+Contents/Resources/9/97/*
+Contents/Resources/9/98/*
+Contents/Resources/9/99/*
+Contents/Resources/9/90/
+Contents/Resources/9/94/*
+Contents/Resources/9/95/*
+Contents/Resources/9/99/
diff --git a/toolkit/mozapps/update/tests/data/partial_update_manifest b/toolkit/mozapps/update/tests/data/partial_update_manifest
new file mode 100644
index 000000000..8d4e60ed2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_update_manifest
@@ -0,0 +1,63 @@
+type "partial"
+add "precomplete"
+add "a/b/searchplugins/searchpluginstext0"
+patch-if "a/b/searchplugins/searchpluginspng1.png" "a/b/searchplugins/searchpluginspng1.png.patch" "a/b/searchplugins/searchpluginspng1.png"
+patch-if "a/b/searchplugins/searchpluginspng0.png" "a/b/searchplugins/searchpluginspng0.png.patch" "a/b/searchplugins/searchpluginspng0.png"
+add-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1text0"
+patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png1.png.patch" "a/b/extensions/extensions1/extensions1png1.png"
+patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png0.png.patch" "a/b/extensions/extensions1/extensions1png0.png"
+add-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0text0"
+patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png1.png.patch" "a/b/extensions/extensions0/extensions0png1.png"
+patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png0.png.patch" "a/b/extensions/extensions0/extensions0png0.png"
+patch "a/b/exe0.exe.patch" "a/b/exe0.exe"
+patch "a/b/0/0exe0.exe.patch" "a/b/0/0exe0.exe"
+add "a/b/0/00/00text0"
+patch "a/b/0/00/00png0.png.patch" "a/b/0/00/00png0.png"
+add "a/b/2/20/20text0"
+add "a/b/2/20/20png0.png"
+add "a/b/0/00/00text2"
+remove "a/b/1/10/10text0"
+remove "a/b/0/00/00text1"
+remove "a/b/text1"
+remove "a/b/text0"
+rmrfdir "a/b/9/99/"
+rmdir "a/b/9/99/"
+rmrfdir "a/b/9/98/"
+rmrfdir "a/b/9/97/"
+rmrfdir "a/b/9/96/"
+rmrfdir "a/b/9/95/"
+rmrfdir "a/b/9/95/"
+rmrfdir "a/b/9/94/"
+rmdir "a/b/9/94/"
+rmdir "a/b/9/93/"
+rmdir "a/b/9/92/"
+rmdir "a/b/9/91/"
+rmdir "a/b/9/90/"
+rmdir "a/b/9/90/"
+rmrfdir "a/b/8/89/"
+rmdir "a/b/8/89/"
+rmrfdir "a/b/8/88/"
+rmrfdir "a/b/8/87/"
+rmrfdir "a/b/8/86/"
+rmrfdir "a/b/8/85/"
+rmrfdir "a/b/8/85/"
+rmrfdir "a/b/8/84/"
+rmdir "a/b/8/84/"
+rmdir "a/b/8/83/"
+rmdir "a/b/8/82/"
+rmdir "a/b/8/81/"
+rmdir "a/b/8/80/"
+rmdir "a/b/8/80/"
+rmrfdir "a/b/7/"
+rmdir "a/b/6/"
+remove "a/b/5/5text1"
+remove "a/b/5/5text0"
+rmrfdir "a/b/5/"
+remove "a/b/4/4text1"
+remove "a/b/4/4text0"
+remove "a/b/4/4exe0.exe"
+rmdir "a/b/4/"
+remove "a/b/3/3text1"
+remove "a/b/3/3text0"
+rmdir "a/b/1/10/"
+rmdir "a/b/1/"
diff --git a/toolkit/mozapps/update/tests/data/replace_log_success b/toolkit/mozapps/update/tests/data/replace_log_success
new file mode 100644
index 000000000..323f1db41
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/replace_log_success
@@ -0,0 +1,6 @@
+Performing a replace request
+rename_file: proceeding to rename the directory
+rename_file: proceeding to rename the directory
+Now, remove the tmpDir
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/shared.js b/toolkit/mozapps/update/tests/data/shared.js
new file mode 100644
index 000000000..e9a10da06
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -0,0 +1,632 @@
+/* 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/. */
+
+/* Shared code for xpcshell and mochitests-chrome */
+/* eslint-disable no-undef */
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+const PREF_APP_UPDATE_AUTO = "app.update.auto";
+const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_APP_UPDATE_DOWNLOADBACKGROUNDINTERVAL = "app.update.download.backgroundInterval";
+const PREF_APP_UPDATE_ENABLED = "app.update.enabled";
+const PREF_APP_UPDATE_IDLETIME = "app.update.idletime";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED = "app.update.notifiedUnsupported";
+const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
+const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout";
+const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
+const PREF_APP_UPDATE_SILENT = "app.update.silent";
+const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
+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 PREFBRANCH_APP_UPDATE_NEVER = "app.update.never.";
+
+const PREFBRANCH_APP_PARTNER = "app.partner.";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+const PREF_TOOLKIT_TELEMETRY_ENABLED = "toolkit.telemetry.enabled";
+
+const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+const NS_GRE_DIR = "GreD";
+const NS_GRE_BIN_DIR = "GreBinD";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_EXECUTABLE_FILE = "XREExeF";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+
+const DIR_PATCH = "0";
+const DIR_TOBEDELETED = "tobedeleted";
+const DIR_UPDATES = "updates";
+const DIR_UPDATED = IS_MACOSX ? "Updated.app" : "updated";
+
+const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
+const FILE_APPLICATION_INI = "application.ini";
+const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
+const FILE_LAST_UPDATE_LOG = "last-update.log";
+const FILE_UPDATE_SETTINGS_INI = "update-settings.ini";
+const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak";
+const FILE_UPDATER_INI = "updater.ini";
+const FILE_UPDATES_XML = "updates.xml";
+const FILE_UPDATE_LOG = "update.log";
+const FILE_UPDATE_MAR = "update.mar";
+const FILE_UPDATE_STATUS = "update.status";
+const FILE_UPDATE_TEST = "update.test";
+const FILE_UPDATE_VERSION = "update.version";
+
+const UPDATE_SETTINGS_CONTENTS = "[Settings]\n" +
+ "ACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n";
+
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+const DEFAULT_UPDATE_VERSION = "999999.0";
+
+var gChannel;
+
+/* import-globals-from ../data/sharedUpdateXML.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this);
+
+const PERMS_FILE = FileUtils.PERMS_FILE;
+const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY;
+
+const MODE_WRONLY = FileUtils.MODE_WRONLY;
+const MODE_CREATE = FileUtils.MODE_CREATE;
+const MODE_APPEND = FileUtils.MODE_APPEND;
+const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE;
+
+const URI_UPDATES_PROPERTIES = "chrome://mozapps/locale/update/updates.properties";
+const gUpdateBundle = Services.strings.createBundle(URI_UPDATES_PROPERTIES);
+
+XPCOMUtils.defineLazyGetter(this, "gAUS", function test_gAUS() {
+ return Cc["@mozilla.org/updates/update-service;1"].
+ getService(Ci.nsIApplicationUpdateService).
+ QueryInterface(Ci.nsITimerCallback).
+ QueryInterface(Ci.nsIObserver).
+ QueryInterface(Ci.nsIUpdateCheckListener);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gUpdateManager",
+ "@mozilla.org/updates/update-manager;1",
+ "nsIUpdateManager");
+
+XPCOMUtils.defineLazyGetter(this, "gUpdateChecker", function test_gUC() {
+ return Cc["@mozilla.org/updates/update-checker;1"].
+ createInstance(Ci.nsIUpdateChecker);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gUP", function test_gUP() {
+ return Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
+ return Services.prefs.getDefaultBranch(null);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() {
+ return Services.prefs.getBranch(null);
+});
+
+XPCOMUtils.defineLazyServiceGetter(this, "gEnv",
+ "@mozilla.org/process/environment;1",
+ "nsIEnvironment");
+
+XPCOMUtils.defineLazyGetter(this, "gZipW", function test_gZipW() {
+ return Cc["@mozilla.org/zipwriter;1"].
+ createInstance(Ci.nsIZipWriter);
+});
+
+/* Triggers post-update processing */
+function testPostUpdateProcessing() {
+ gAUS.observe(null, "test-post-update-processing", "");
+}
+
+/* Initializes the update service stub */
+function initUpdateServiceStub() {
+ Cc["@mozilla.org/updates/update-service-stub;1"].
+ createInstance(Ci.nsISupports);
+}
+
+/* Reloads the update metadata from disk */
+function reloadUpdateManagerData() {
+ gUpdateManager.QueryInterface(Ci.nsIObserver).
+ observe(null, "um-reload-update-data", "");
+}
+
+const observer = {
+ observe: function(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed" && aData == PREF_APP_UPDATE_CHANNEL) {
+ let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ if (channel != gChannel) {
+ debugDump("Changing channel from " + channel + " to " + gChannel);
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel);
+ }
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+
+/**
+ * Sets the app.update.channel preference.
+ *
+ * @param aChannel
+ * The update channel.
+ */
+function setUpdateChannel(aChannel) {
+ gChannel = aChannel;
+ debugDump("setting default pref " + PREF_APP_UPDATE_CHANNEL + " to " + gChannel);
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel);
+ gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer, false);
+}
+
+/**
+ * Sets the app.update.url default preference.
+ *
+ * @param aURL
+ * The update url. If not specified 'URL_HOST + "/update.xml"' will be
+ * used.
+ */
+function setUpdateURL(aURL) {
+ let url = aURL ? aURL : URL_HOST + "/update.xml";
+ debugDump("setting " + PREF_APP_UPDATE_URL + " to " + url);
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_URL, url);
+}
+
+/**
+ * Returns either the active or regular update database XML file.
+ *
+ * @param isActiveUpdate
+ * If true this will return the active-update.xml otherwise it will
+ * return the updates.xml file.
+ */
+function getUpdatesXMLFile(aIsActiveUpdate) {
+ let file = getUpdatesRootDir();
+ file.append(aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML);
+ return file;
+}
+
+/**
+ * Writes the updates specified to either the active-update.xml or the
+ * updates.xml.
+ *
+ * @param aContent
+ * The updates represented as a string to write to the XML file.
+ * @param isActiveUpdate
+ * If true this will write to the active-update.xml otherwise it will
+ * write to the updates.xml file.
+ */
+function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) {
+ writeFile(getUpdatesXMLFile(aIsActiveUpdate), aContent);
+}
+
+/**
+ * Writes the current update operation/state to a file in the patch
+ * directory, indicating to the patching system that operations need
+ * to be performed.
+ *
+ * @param aStatus
+ * The status value to write.
+ */
+function writeStatusFile(aStatus) {
+ let file = getUpdatesPatchDir();
+ file.append(FILE_UPDATE_STATUS);
+ writeFile(file, aStatus + "\n");
+}
+
+/**
+ * Writes the current update version to a file in the patch directory,
+ * indicating to the patching system the version of the update.
+ *
+ * @param aVersion
+ * The version value to write.
+ */
+function writeVersionFile(aVersion) {
+ let file = getUpdatesPatchDir();
+ file.append(FILE_UPDATE_VERSION);
+ writeFile(file, aVersion + "\n");
+}
+
+/**
+ * Gets the root directory for the updates directory.
+ *
+ * @return nsIFile for the updates root directory.
+ */
+function getUpdatesRootDir() {
+ return Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the updates directory.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesDir() {
+ let dir = getUpdatesRootDir();
+ dir.append(DIR_UPDATES);
+ return dir;
+}
+
+/**
+ * Gets the directory for update patches.
+ *
+ * @return nsIFile for the updates directory.
+ */
+function getUpdatesPatchDir() {
+ let dir = getUpdatesDir();
+ dir.append(DIR_PATCH);
+ return dir;
+}
+
+/**
+ * Writes text to a file. This will replace existing text if the file exists
+ * and create the file if it doesn't exist.
+ *
+ * @param aFile
+ * The file to write to. Will be created if it doesn't exist.
+ * @param aText
+ * The text to write to the file. If there is existing text it will be
+ * replaced.
+ */
+function writeFile(aFile, aText) {
+ let fos = Cc["@mozilla.org/network/file-output-stream;1"].
+ createInstance(Ci.nsIFileOutputStream);
+ if (!aFile.exists()) {
+ aFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0);
+ fos.write(aText, aText.length);
+ fos.close();
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory including the error code if it is present.
+ *
+ * @return The status value.
+ */
+function readStatusFile() {
+ let file = getUpdatesPatchDir();
+ file.append(FILE_UPDATE_STATUS);
+
+ if (!file.exists()) {
+ debugDump("update status file does not exists! Path: " + file.path);
+ return STATE_NONE;
+ }
+
+ return readFile(file).split("\n")[0];
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory without the error code if it is present.
+ *
+ * @return The state value.
+ */
+function readStatusState() {
+ return readStatusFile().split(": ")[0];
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory with the error code.
+ *
+ * @return The state value.
+ */
+function readStatusFailedCode() {
+ return readStatusFile().split(": ")[1];
+}
+
+/**
+ * Reads text from a file and returns the string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The string of text read from the file.
+ */
+function readFile(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ if (!aFile.exists()) {
+ return null;
+ }
+ // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY.
+ // Specifying -1 for perm will open the file with the default of 0.
+ fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(fis);
+ let text = sis.read(sis.available());
+ sis.close();
+ return text;
+}
+
+/**
+ * Reads the binary contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The contents of the file as a string.
+ */
+function readFileBytes(aFile) {
+ debugDump("attempting to read file, path: " + aFile.path);
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY.
+ // Specifying -1 for perm will open the file with the default of 0.
+ fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ let bis = Cc["@mozilla.org/binaryinputstream;1"].
+ createInstance(Ci.nsIBinaryInputStream);
+ bis.setInputStream(fis);
+ let data = [];
+ let count = fis.available();
+ while (count > 0) {
+ let bytes = bis.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (bytes.length == 0) {
+ throw "Nothing read from input stream!";
+ }
+ }
+ data.join('');
+ fis.close();
+ return data.toString();
+}
+
+/* Returns human readable status text from the updates.properties bundle */
+function getStatusText(aErrCode) {
+ return getString("check_error-" + aErrCode);
+}
+
+/* Returns a string from the updates.properties bundle */
+function getString(aName) {
+ try {
+ return gUpdateBundle.GetStringFromName(aName);
+ } catch (e) {
+ }
+ return null;
+}
+
+/**
+ * Gets the file extension for an nsIFile.
+ *
+ * @param aFile
+ * The file to get the file extension for.
+ * @return The file extension.
+ */
+function getFileExtension(aFile) {
+ return Services.io.newFileURI(aFile).QueryInterface(Ci.nsIURL).
+ fileExtension;
+}
+
+/**
+ * Removes the updates.xml file, active-update.xml file, and all files and
+ * sub-directories in the updates directory except for the "0" sub-directory.
+ * This prevents some tests from failing due to files being left behind when the
+ * tests are interrupted.
+ */
+function removeUpdateDirsAndFiles() {
+ let file = getUpdatesXMLFile(true);
+ try {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (e) {
+ logTestInfo("Unable to remove file. Path: " + file.path +
+ ", Exception: " + e);
+ }
+
+ file = getUpdatesXMLFile(false);
+ try {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (e) {
+ logTestInfo("Unable to remove file. Path: " + file.path +
+ ", Exception: " + e);
+ }
+
+ // This fails sporadically on Mac OS X so wrap it in a try catch
+ let updatesDir = getUpdatesDir();
+ try {
+ cleanUpdatesDir(updatesDir);
+ } catch (e) {
+ logTestInfo("Unable to remove files / directories from directory. Path: " +
+ updatesDir.path + ", Exception: " + e);
+ }
+}
+
+/**
+ * Removes all files and sub-directories in the updates directory except for
+ * the "0" sub-directory.
+ *
+ * @param aDir
+ * nsIFile for the directory to be deleted.
+ */
+function cleanUpdatesDir(aDir) {
+ if (!aDir.exists()) {
+ return;
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.isDirectory()) {
+ if (entry.leafName == DIR_PATCH && entry.parent.leafName == DIR_UPDATES) {
+ cleanUpdatesDir(entry);
+ entry.permissions = PERMS_DIRECTORY;
+ } else {
+ try {
+ entry.remove(true);
+ return;
+ } catch (e) {
+ }
+ cleanUpdatesDir(entry);
+ entry.permissions = PERMS_DIRECTORY;
+ try {
+ entry.remove(true);
+ } catch (e) {
+ logTestInfo("cleanUpdatesDir: unable to remove directory. Path: " +
+ entry.path + ", Exception: " + e);
+ throw (e);
+ }
+ }
+ } else {
+ entry.permissions = PERMS_FILE;
+ try {
+ entry.remove(false);
+ } catch (e) {
+ logTestInfo("cleanUpdatesDir: unable to remove file. Path: " +
+ entry.path + ", Exception: " + e);
+ throw (e);
+ }
+ }
+ }
+}
+
+/**
+ * Deletes a directory and its children. First it tries nsIFile::Remove(true).
+ * If that fails it will fall back to recursing, setting the appropriate
+ * permissions, and deleting the current entry.
+ *
+ * @param aDir
+ * nsIFile for the directory to be deleted.
+ */
+function removeDirRecursive(aDir) {
+ if (!aDir.exists()) {
+ return;
+ }
+
+ try {
+ debugDump("attempting to remove directory. Path: " + aDir.path);
+ aDir.remove(true);
+ return;
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Exception: " + e);
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.isDirectory()) {
+ removeDirRecursive(entry);
+ } else {
+ entry.permissions = PERMS_FILE;
+ try {
+ debugDump("attempting to remove file. Path: " + entry.path);
+ entry.remove(false);
+ } catch (e) {
+ logTestInfo("error removing file. Exception: " + e);
+ throw (e);
+ }
+ }
+ }
+
+ aDir.permissions = PERMS_DIRECTORY;
+ try {
+ debugDump("attempting to remove directory. Path: " + aDir.path);
+ aDir.remove(true);
+ } catch (e) {
+ logTestInfo("error removing directory. Exception: " + e);
+ throw (e);
+ }
+}
+
+/**
+ * Returns the directory for the currently running process. This is used to
+ * clean up after the tests and to locate the active-update.xml and updates.xml
+ * files.
+ *
+ * @return nsIFile for the current process directory.
+ */
+function getCurrentProcessDir() {
+ return Services.dirsvc.get(NS_XPCOM_CURRENT_PROCESS_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the application base directory.
+ *
+ * @return nsIFile object for the application base directory.
+ */
+function getAppBaseDir() {
+ return Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent;
+}
+
+/**
+ * Returns the Gecko Runtime Engine directory where files other than executable
+ * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/
+ * and the installation directory on all other platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine directory.
+ */
+function getGREDir() {
+ return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile);
+}
+
+/**
+ * Returns the Gecko Runtime Engine Binary directory where the executable
+ * binaries are located such as the updater binary (Windows and Linux) or
+ * updater package (Mac OS X). On Mac OS X this will be
+ * <bundle>/Contents/MacOS/ and the installation directory on all other
+ * platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine Binary directory.
+ */
+function getGREBinDir() {
+ return Services.dirsvc.get(NS_GRE_BIN_DIR, Ci.nsIFile);
+}
+
+/**
+ * Logs TEST-INFO messages.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function logTestInfo(aText, aCaller) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ let now = new Date();
+ let hh = now.getHours();
+ let mm = now.getMinutes();
+ let ss = now.getSeconds();
+ let ms = now.getMilliseconds();
+ let time = (hh < 10 ? "0" + hh : hh) + ":" +
+ (mm < 10 ? "0" + mm : mm) + ":" +
+ (ss < 10 ? "0" + ss : ss) + ":";
+ if (ms < 10) {
+ time += "00";
+ } else if (ms < 100) {
+ time += "0";
+ }
+ time += ms;
+ let msg = time + " | TEST-INFO | " + caller.filename + " | [" + caller.name +
+ " : " + caller.lineNumber + "] " + aText;
+ LOG_FUNCTION(msg);
+}
+
+/**
+ * Logs TEST-INFO messages when DEBUG_AUS_TEST evaluates to true.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function debugDump(aText, aCaller) {
+ if (DEBUG_AUS_TEST) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ logTestInfo(aText, caller);
+ }
+}
diff --git a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
new file mode 100644
index 000000000..3aa01eff4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
@@ -0,0 +1,364 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Helper functions for creating xml strings used by application update tests.
+ *
+ * !IMPORTANT - This file contains everything needed (along with dependencies)
+ * by the updates.sjs file used by the mochitest-chrome tests. Since xpcshell
+ * used by the http server is launched with -v 170 this file must not use
+ * features greater than JavaScript 1.7.
+ */
+
+/* eslint-disable no-undef */
+
+const FILE_SIMPLE_MAR = "simple.mar";
+const SIZE_SIMPLE_MAR = "1031";
+const MD5_HASH_SIMPLE_MAR = "1f8c038577bb6845d94ccec4999113ee";
+const SHA1_HASH_SIMPLE_MAR = "5d49a672c87f10f31d7e326349564a11272a028b";
+const SHA256_HASH_SIMPLE_MAR = "1aabbed5b1dd6e16e139afc5b43d479e254e0c26" +
+ "3c8fb9249c0a1bb93071c5fb";
+const SHA384_HASH_SIMPLE_MAR = "26615014ea034af32ef5651492d5f493f5a7a1a48522e" +
+ "d24c366442a5ec21d5ef02e23fb58d79729b8ca2f9541" +
+ "99dd53";
+const SHA512_HASH_SIMPLE_MAR = "922e5ae22081795f6e8d65a3c508715c9a314054179a8" +
+ "bbfe5f50dc23919ad89888291bc0a07586ab17dd0304a" +
+ "b5347473601127571c66f61f5080348e05c36b";
+
+const STATE_NONE = "null";
+const STATE_DOWNLOADING = "downloading";
+const STATE_PENDING = "pending";
+const STATE_PENDING_SVC = "pending-service";
+const STATE_APPLYING = "applying";
+const STATE_APPLIED = "applied";
+const STATE_APPLIED_SVC = "applied-service";
+const STATE_SUCCEEDED = "succeeded";
+const STATE_DOWNLOAD_FAILED = "download-failed";
+const STATE_FAILED = "failed";
+
+const LOADSOURCE_ERROR_WRONG_SIZE = 2;
+const CRC_ERROR = 4;
+const READ_ERROR = 6;
+const WRITE_ERROR = 7;
+const MAR_CHANNEL_MISMATCH_ERROR = 22;
+const VERSION_DOWNGRADE_ERROR = 23;
+const SERVICE_COULD_NOT_COPY_UPDATER = 49;
+const SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR = 52;
+const SERVICE_INVALID_APPLYTO_DIR_ERROR = 54;
+const SERVICE_INVALID_INSTALL_DIR_PATH_ERROR = 55;
+const SERVICE_INVALID_WORKING_DIR_PATH_ERROR = 56;
+const INVALID_APPLYTO_DIR_STAGED_ERROR = 72;
+const INVALID_APPLYTO_DIR_ERROR = 74;
+const INVALID_INSTALL_DIR_PATH_ERROR = 75;
+const INVALID_WORKING_DIR_PATH_ERROR = 76;
+const INVALID_CALLBACK_PATH_ERROR = 77;
+const INVALID_CALLBACK_DIR_ERROR = 78;
+
+const STATE_FAILED_DELIMETER = ": ";
+
+const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE =
+ STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE;
+const STATE_FAILED_CRC_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + CRC_ERROR;
+const STATE_FAILED_READ_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR;
+const STATE_FAILED_WRITE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR;
+const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR;
+const STATE_FAILED_VERSION_DOWNGRADE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR;
+const STATE_FAILED_SERVICE_COULD_NOT_COPY_UPDATER =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_COULD_NOT_COPY_UPDATER
+const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_ERROR;
+const STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_INSTALL_DIR_PATH_ERROR;
+const STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_WORKING_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR;
+const STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_INSTALL_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_WORKING_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_CALLBACK_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_PATH_ERROR;
+const STATE_FAILED_INVALID_CALLBACK_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_DIR_ERROR;
+
+/**
+ * Constructs a string representing a remote update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a remote update xml file.
+ */
+function getRemoteUpdatesXMLString(aUpdates) {
+ return "<?xml version=\"1.0\"?>\n" +
+ "<updates>\n" +
+ aUpdates +
+ "</updates>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aPatches
+ * String representing the application update patches.
+ * @return The string representing an update element for an update xml file.
+ */
+function getRemoteUpdateString(aPatches, aType, aName, aDisplayVersion,
+ aAppVersion, aBuildID, aDetailsURL, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aCustom1, aCustom2) {
+ return getUpdateString(aType, aName, aDisplayVersion, aAppVersion,
+ aBuildID, aDetailsURL, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aCustom1, aCustom2) + ">\n" +
+ aPatches +
+ " </update>\n";
+}
+
+/**
+ * Constructs a string representing a patch element for a remote update xml
+ * file. See getPatchString for parameter information not provided below.
+ *
+ * @return The string representing a patch element for a remote update xml file.
+ */
+function getRemotePatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+ return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) +
+ "/>\n";
+}
+
+/**
+ * Constructs a string representing a local update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a local update xml file.
+ */
+function getLocalUpdatesXMLString(aUpdates) {
+ if (!aUpdates || aUpdates == "") {
+ return "<updates xmlns=\"http://www.mozilla.org/2005/app-update\"/>";
+ }
+ return ("<updates xmlns=\"http://www.mozilla.org/2005/app-update\">" +
+ aUpdates +
+ "</updates>").replace(/>\s+\n*</g, '><');
+}
+
+/**
+ * Constructs a string representing an update element for a local update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aPatches
+ * String representing the application update patches.
+ * @param aServiceURL (optional)
+ * The update's xml url.
+ * If not specified it will default to 'http://test_service/'.
+ * @param aIsCompleteUpdate (optional)
+ * The string 'true' if this update was a complete update or the string
+ * 'false' if this update was a partial update.
+ * If not specified it will default to 'true'.
+ * @param aChannel (optional)
+ * The update channel name.
+ * If not specified it will default to the default preference value of
+ * app.update.channel.
+ * @param aForegroundDownload (optional)
+ * The string 'true' if this update was manually downloaded or the
+ * string 'false' if this update was automatically downloaded.
+ * If not specified it will default to 'true'.
+ * @param aPreviousAppVersion (optional)
+ * The application version prior to applying the update.
+ * If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getLocalUpdateString(aPatches, aType, aName, aDisplayVersion,
+ aAppVersion, aBuildID, aDetailsURL, aServiceURL,
+ aInstallDate, aStatusText, aIsCompleteUpdate,
+ aChannel, aForegroundDownload, aShowPrompt,
+ aShowNeverForVersion, aPromptWaitTime,
+ aBackgroundInterval, aPreviousAppVersion,
+ aCustom1, aCustom2) {
+ let serviceURL = aServiceURL ? aServiceURL : "http://test_service/";
+ let installDate = aInstallDate ? aInstallDate : "1238441400314";
+ let statusText = aStatusText ? aStatusText : "Install Pending";
+ let isCompleteUpdate =
+ typeof aIsCompleteUpdate == "string" ? aIsCompleteUpdate : "true";
+ let channel = aChannel ? aChannel
+ : gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ let foregroundDownload =
+ typeof aForegroundDownload == "string" ? aForegroundDownload : "true";
+ let previousAppVersion = aPreviousAppVersion ? "previousAppVersion=\"" +
+ aPreviousAppVersion + "\" "
+ : "";
+ return getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+ aDetailsURL, aShowPrompt, aShowNeverForVersion,
+ aPromptWaitTime, aBackgroundInterval, aCustom1, aCustom2) +
+ " " +
+ previousAppVersion +
+ "serviceURL=\"" + serviceURL + "\" " +
+ "installDate=\"" + installDate + "\" " +
+ "statusText=\"" + statusText + "\" " +
+ "isCompleteUpdate=\"" + isCompleteUpdate + "\" " +
+ "channel=\"" + channel + "\" " +
+ "foregroundDownload=\"" + foregroundDownload + "\">" +
+ aPatches +
+ " </update>";
+}
+
+/**
+ * Constructs a string representing a patch element for a local update xml file.
+ * See getPatchString for parameter information not provided below.
+ *
+ * @param aSelected (optional)
+ * Whether this patch is selected represented or not. The string 'true'
+ * denotes selected and the string 'false' denotes not selected.
+ * If not specified it will default to the string 'true'.
+ * @param aState (optional)
+ * The patch's state.
+ * If not specified it will default to STATE_SUCCEEDED.
+ * @return The string representing a patch element for a local update xml file.
+ */
+function getLocalPatchString(aType, aURL, aHashFunction, aHashValue, aSize,
+ aSelected, aState) {
+ let selected = typeof aSelected == "string" ? aSelected : "true";
+ let state = aState ? aState : STATE_SUCCEEDED;
+ return getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) + " " +
+ "selected=\"" + selected + "\" " +
+ "state=\"" + state + "\"/>\n";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file.
+ *
+ * @param aType (optional)
+ * The update's type which should be major or minor. If not specified it
+ * will default to 'major'.
+ * @param aName (optional)
+ * The update's name.
+ * If not specified it will default to 'App Update Test'.
+ * @param aDisplayVersion (optional)
+ * The update's display version.
+ * If not specified it will default to 'version #' where # is the value
+ * of DEFAULT_UPDATE_VERSION.
+ * @param aAppVersion (optional)
+ * The update's application version.
+ * If not specified it will default to the value of
+ * DEFAULT_UPDATE_VERSION.
+ * @param aBuildID (optional)
+ * The update's build id.
+ * If not specified it will default to '20080811053724'.
+ * @param aDetailsURL (optional)
+ * The update's details url.
+ * If not specified it will default to 'http://test_details/' due to due
+ * to bug 470244.
+ * @param aShowPrompt (optional)
+ * Whether to show the prompt for the update when auto update is
+ * enabled.
+ * If not specified it will not be present and the update service will
+ * default to false.
+ * @param aShowNeverForVersion (optional)
+ * Whether to show the 'No Thanks' button in the update prompt.
+ * If not specified it will not be present and the update service will
+ * default to false.
+ * @param aPromptWaitTime (optional)
+ * Override for the app.update.promptWaitTime preference.
+ * @param aBackgroundInterval (optional)
+ * Override for the app.update.download.backgroundInterval preference.
+ * @param aCustom1 (optional)
+ * A custom attribute name and attribute value to add to the xml.
+ * Example: custom1_attribute="custom1 value"
+ * If not specified it will not be present.
+ * @param aCustom2 (optional)
+ * A custom attribute name and attribute value to add to the xml.
+ * Example: custom2_attribute="custom2 value"
+ * If not specified it will not be present.
+ * @return The string representing an update element for an update xml file.
+ */
+function getUpdateString(aType, aName, aDisplayVersion, aAppVersion, aBuildID,
+ aDetailsURL, aShowPrompt, aShowNeverForVersion,
+ aPromptWaitTime, aBackgroundInterval, aCustom1,
+ aCustom2) {
+ let type = aType ? aType : "major";
+ let name = aName ? aName : "App Update Test";
+ let displayVersion = aDisplayVersion ? "displayVersion=\"" +
+ aDisplayVersion + "\" "
+ : "";
+ let appVersion = "appVersion=\"" +
+ (aAppVersion ? aAppVersion : DEFAULT_UPDATE_VERSION) +
+ "\" ";
+ let buildID = aBuildID ? aBuildID : "20080811053724";
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+// let detailsURL = aDetailsURL ? "detailsURL=\"" + aDetailsURL + "\" " : "";
+ let detailsURL = "detailsURL=\"" +
+ (aDetailsURL ? aDetailsURL
+ : "http://test_details/") + "\" ";
+ let showPrompt = aShowPrompt ? "showPrompt=\"" + aShowPrompt + "\" " : "";
+ let showNeverForVersion = aShowNeverForVersion ? "showNeverForVersion=\"" +
+ aShowNeverForVersion + "\" "
+ : "";
+ let promptWaitTime = aPromptWaitTime ? "promptWaitTime=\"" + aPromptWaitTime +
+ "\" "
+ : "";
+ let backgroundInterval = aBackgroundInterval ? "backgroundInterval=\"" +
+ aBackgroundInterval + "\" "
+ : "";
+ let custom1 = aCustom1 ? aCustom1 + " " : "";
+ let custom2 = aCustom2 ? aCustom2 + " " : "";
+ return " <update type=\"" + type + "\" " +
+ "name=\"" + name + "\" " +
+ displayVersion +
+ appVersion +
+ detailsURL +
+ showPrompt +
+ showNeverForVersion +
+ promptWaitTime +
+ backgroundInterval +
+ custom1 +
+ custom2 +
+ "buildID=\"" + buildID + "\"";
+}
+
+/**
+ * Constructs a string representing a patch element for an update xml file.
+ *
+ * @param aType (optional)
+ * The patch's type which should be complete or partial.
+ * If not specified it will default to 'complete'.
+ * @param aURL (optional)
+ * The patch's url to the mar file.
+ * If not specified it will default to the value of:
+ * gURLData + FILE_SIMPLE_MAR
+ * @param aHashFunction (optional)
+ * The patch's hash function used to verify the mar file.
+ * If not specified it will default to 'MD5'.
+ * @param aHashValue (optional)
+ * The patch's hash value used to verify the mar file.
+ * If not specified it will default to the value of MD5_HASH_SIMPLE_MAR
+ * which is the MD5 hash value for the file specified by FILE_SIMPLE_MAR.
+ * @param aSize (optional)
+ * The patch's file size for the mar file.
+ * If not specified it will default to the file size for FILE_SIMPLE_MAR
+ * specified by SIZE_SIMPLE_MAR.
+ * @return The string representing a patch element for an update xml file.
+ */
+function getPatchString(aType, aURL, aHashFunction, aHashValue, aSize) {
+ let type = aType ? aType : "complete";
+ let url = aURL ? aURL : gURLData + FILE_SIMPLE_MAR;
+ let hashFunction = aHashFunction ? aHashFunction : "MD5";
+ let hashValue = aHashValue ? aHashValue : MD5_HASH_SIMPLE_MAR;
+ let size = aSize ? aSize : SIZE_SIMPLE_MAR;
+ return " <patch type=\"" + type + "\" " +
+ "URL=\"" + url + "\" " +
+ "hashFunction=\"" + hashFunction + "\" " +
+ "hashValue=\"" + hashValue + "\" " +
+ "size=\"" + size + "\"";
+}
diff --git a/toolkit/mozapps/update/tests/data/simple.mar b/toolkit/mozapps/update/tests/data/simple.mar
new file mode 100644
index 000000000..b2ccbd8d2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/simple.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/wrong_product_channel.mar b/toolkit/mozapps/update/tests/data/wrong_product_channel.mar
new file mode 100644
index 000000000..1e39cc214
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/wrong_product_channel.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
new file mode 100644
index 000000000..e5f0fce58
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
@@ -0,0 +1,53 @@
+/* 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/. */
+
+/* Preprocessed constants used by xpcshell tests */
+
+const INSTALL_LOCALE = "@AB_CD@";
+const MOZ_APP_NAME = "@MOZ_APP_NAME@";
+const BIN_SUFFIX = "@BIN_SUFFIX@";
+
+// MOZ_APP_VENDOR is optional.
+#ifdef MOZ_APP_VENDOR
+const MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@";
+#else
+const MOZ_APP_VENDOR = "";
+#endif
+
+// MOZ_APP_BASENAME is not optional for tests.
+const MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@";
+const APP_BIN_SUFFIX = "@BIN_SUFFIX@";
+
+const APP_INFO_NAME = "XPCShell";
+const APP_INFO_VENDOR = "Mozilla";
+
+#ifdef XP_WIN
+const IS_WIN = true;
+#else
+const IS_WIN = false;
+#endif
+
+#ifdef XP_MACOSX
+const IS_MACOSX = true;
+#else
+const IS_MACOSX = false;
+#endif
+
+#ifdef XP_UNIX
+const IS_UNIX = true;
+#else
+const IS_UNIX = false;
+#endif
+
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+const MOZ_VERIFY_MAR_SIGNATURE = true;
+#else
+const MOZ_VERIFY_MAR_SIGNATURE = false;
+#endif
+
+#ifdef DISABLE_UPDATER_AUTHENTICODE_CHECK
+ const IS_AUTHENTICODE_CHECK_ENABLED = false;
+#else
+ const IS_AUTHENTICODE_CHECK_ENABLED = true;
+#endif
diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
new file mode 100644
index 000000000..ada08f0ae
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
@@ -0,0 +1,4047 @@
+/* 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/. */
+
+/**
+ * Test log warnings that happen before the test has started
+ * "Couldn't get the user appdata directory. Crash events may not be produced."
+ * in nsExceptionHandler.cpp (possibly bug 619104)
+ *
+ * Test log warnings that happen after the test has finished
+ * "OOPDeinit() without successful OOPInit()" in nsExceptionHandler.cpp
+ * (bug 619104)
+ * "XPCOM objects created/destroyed from static ctor/dtor" in nsTraceRefcnt.cpp
+ * (possibly bug 457479)
+ *
+ * Other warnings printed to the test logs
+ * "site security information will not be persisted" in
+ * nsSiteSecurityService.cpp and the error in nsSystemInfo.cpp preceding this
+ * error are due to not having a profile when running some of the xpcshell
+ * tests. Since most xpcshell tests also log these errors these tests don't
+ * call do_get_profile unless necessary for the test.
+ * The "This method is lossy. Use GetCanonicalPath !" warning on Windows in
+ * nsLocalFileWin.cpp is from the call to GetNSSProfilePath in
+ * nsNSSComponent.cpp due to it using GetNativeCanonicalPath.
+ * "!mMainThread" in nsThreadManager.cpp are due to using timers and it might be
+ * possible to fix some or all of these in the test itself.
+ * "NS_FAILED(rv)" in nsThreadUtils.cpp are due to using timers and it might be
+ * possible to fix some or all of these in the test itself.
+ */
+
+'use strict';
+/* eslint-disable no-undef */
+
+const { classes: Cc, interfaces: Ci, manager: Cm, results: Cr,
+ utils: Cu } = Components;
+
+/* global INSTALL_LOCALE, MOZ_APP_NAME, BIN_SUFFIX, MOZ_APP_VENDOR */
+/* global MOZ_APP_BASENAME, APP_BIN_SUFFIX, APP_INFO_NAME, APP_INFO_VENDOR */
+/* global IS_WIN, IS_MACOSX, IS_UNIX, MOZ_VERIFY_MAR_SIGNATURE */
+/* global IS_AUTHENTICODE_CHECK_ENABLED */
+load("../data/xpcshellConstantsPP.js");
+
+function getLogSuffix() {
+ if (IS_WIN) {
+ return "_win";
+ }
+ if (IS_MACOSX) {
+ return "_mac";
+ }
+ return "_linux";
+}
+
+Cu.import("resource://gre/modules/Services.jsm", this);
+Cu.import("resource://gre/modules/ctypes.jsm", this);
+
+const DIR_MACOS = IS_MACOSX ? "Contents/MacOS/" : "";
+const DIR_RESOURCES = IS_MACOSX ? "Contents/Resources/" : "";
+const TEST_FILE_SUFFIX = IS_MACOSX ? "_mac" : "";
+const FILE_COMPLETE_MAR = "complete" + TEST_FILE_SUFFIX + ".mar";
+const FILE_PARTIAL_MAR = "partial" + TEST_FILE_SUFFIX + ".mar";
+const FILE_COMPLETE_PRECOMPLETE = "complete_precomplete" + TEST_FILE_SUFFIX;
+const FILE_PARTIAL_PRECOMPLETE = "partial_precomplete" + TEST_FILE_SUFFIX;
+const FILE_COMPLETE_REMOVEDFILES = "complete_removed-files" + TEST_FILE_SUFFIX;
+const FILE_PARTIAL_REMOVEDFILES = "partial_removed-files" + TEST_FILE_SUFFIX;
+const FILE_UPDATE_IN_PROGRESS_LOCK = "updated.update_in_progress.lock";
+const COMPARE_LOG_SUFFIX = getLogSuffix();
+const LOG_COMPLETE_SUCCESS = "complete_log_success" + COMPARE_LOG_SUFFIX;
+const LOG_PARTIAL_SUCCESS = "partial_log_success" + COMPARE_LOG_SUFFIX;
+const LOG_PARTIAL_FAILURE = "partial_log_failure" + COMPARE_LOG_SUFFIX;
+const LOG_REPLACE_SUCCESS = "replace_log_success";
+
+const USE_EXECV = IS_UNIX && !IS_MACOSX;
+
+const URL_HOST = "http://localhost";
+
+const FILE_APP_BIN = MOZ_APP_NAME + APP_BIN_SUFFIX;
+const FILE_COMPLETE_EXE = "complete.exe";
+const FILE_HELPER_BIN = "TestAUSHelper" + BIN_SUFFIX;
+const FILE_MAINTENANCE_SERVICE_BIN = "maintenanceservice.exe";
+const FILE_MAINTENANCE_SERVICE_INSTALLER_BIN = "maintenanceservice_installer.exe";
+const FILE_OLD_VERSION_MAR = "old_version.mar";
+const FILE_PARTIAL_EXE = "partial.exe";
+const FILE_UPDATER_BIN = "updater" + BIN_SUFFIX;
+const FILE_WRONG_CHANNEL_MAR = "wrong_product_channel.mar";
+
+const PERFORMING_STAGED_UPDATE = "Performing a staged update";
+const CALL_QUIT = "calling QuitProgressUI";
+const REMOVE_OLD_DIST_DIR = "removing old distribution directory";
+const MOVE_OLD_DIST_DIR = "Moving old distribution directory to new location";
+const ERR_UPDATE_IN_PROGRESS = "Update already in progress! Exiting";
+const ERR_RENAME_FILE = "rename_file: failed to rename file";
+const ERR_ENSURE_COPY = "ensure_copy: failed to copy the file";
+const ERR_UNABLE_OPEN_DEST = "unable to open destination file";
+const ERR_BACKUP_DISCARD = "backup_discard: unable to remove";
+const ERR_MOVE_DESTDIR_7 = "Moving destDir to tmpDir failed, err: 7";
+const ERR_BACKUP_CREATE_7 = "backup_create failed: 7";
+const ERR_LOADSOURCEFILE_FAILED = "LoadSourceFile failed";
+
+const LOG_SVC_SUCCESSFUL_LAUNCH = "Process was started... waiting on result.";
+
+// Typical end of a message when calling assert
+const MSG_SHOULD_EQUAL = " should equal the expected value";
+const MSG_SHOULD_EXIST = "the file or directory should exist";
+const MSG_SHOULD_NOT_EXIST = "the file or directory should not exist";
+
+// All we care about is that the last modified time has changed so that Mac OS
+// X Launch Services invalidates its cache so the test allows up to one minute
+// difference in the last modified time.
+const MAC_MAX_TIME_DIFFERENCE = 60000;
+
+// How many of do_execute_soon calls to wait before the test is aborted.
+const MAX_TIMEOUT_RUNS = 20000;
+
+// Time in seconds the helper application should sleep before exiting. The
+// helper can also be made to exit by writing |finish| to its input file.
+const HELPER_SLEEP_TIMEOUT = 180;
+
+// Maximum number of milliseconds the process that is launched can run before
+// the test will try to kill it.
+const APP_TIMER_TIMEOUT = 120000;
+
+// How many of do_timeout calls using FILE_IN_USE_TIMEOUT_MS to wait before the
+// test is aborted.
+const FILE_IN_USE_MAX_TIMEOUT_RUNS = 60;
+const FILE_IN_USE_TIMEOUT_MS = 1000;
+
+const PIPE_TO_NULL = IS_WIN ? ">nul" : "> /dev/null 2>&1";
+
+const LOG_FUNCTION = do_print;
+
+const gHTTPHandlerPath = "updates.xml";
+
+// This default value will be overridden when using the http server.
+var gURLData = URL_HOST + "/";
+
+var gTestID;
+
+var gTestserver;
+
+var gRegisteredServiceCleanup;
+
+var gCheckFunc;
+var gResponseBody;
+var gResponseStatusCode = 200;
+var gRequestURL;
+var gUpdateCount;
+var gUpdates;
+var gStatusCode;
+var gStatusText;
+var gStatusResult;
+
+var gProcess;
+var gAppTimer;
+var gHandle;
+
+var gGREDirOrig;
+var gGREBinDirOrig;
+var gAppDirOrig;
+
+// Variables are used instead of contants so tests can override these values if
+// necessary.
+var gCallbackBinFile = "callback_app" + BIN_SUFFIX;
+var gCallbackArgs = ["./", "callback.log", "Test Arg 2", "Test Arg 3"];
+var gPostUpdateBinFile = "postup_app" + BIN_SUFFIX;
+var gSvcOriginalLogContents;
+var gUseTestAppDir = true;
+// Some update staging failures can remove the update. This allows tests to
+// specify that the status file and the active update should not be checked
+// after an update is staged.
+var gStagingRemovedUpdate = false;
+
+var gTimeoutRuns = 0;
+var gFileInUseTimeoutRuns = 0;
+
+// Environment related globals
+var gShouldResetEnv = undefined;
+var gAddedEnvXRENoWindowsCrashDialog = false;
+var gEnvXPCOMDebugBreak;
+var gEnvXPCOMMemLeakLog;
+var gEnvDyldLibraryPath;
+var gEnvLdLibraryPath;
+var gASanOptions;
+
+// Set to true to log additional information for debugging. To log additional
+// information for an individual test set DEBUG_AUS_TEST to true in the test's
+// run_test function.
+var DEBUG_AUS_TEST = true;
+
+const DATA_URI_SPEC = Services.io.newFileURI(do_get_file("../data", false)).spec;
+/* import-globals-from ../data/shared.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "shared.js", this);
+
+var gTestFiles = [];
+var gTestDirs = [];
+
+// Common files for both successful and failed updates.
+var gTestFilesCommon = [
+ {
+ description: "Should never change",
+ fileName: FILE_UPDATE_SETTINGS_INI,
+ relPathDir: DIR_RESOURCES,
+ originalContents: UPDATE_SETTINGS_CONTENTS,
+ compareContents: UPDATE_SETTINGS_CONTENTS,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o767
+ }, {
+ description: "Should never change",
+ fileName: "channel-prefs.js",
+ relPathDir: DIR_RESOURCES + "defaults/pref/",
+ originalContents: "ShouldNotBeReplaced\n",
+ compareContents: "ShouldNotBeReplaced\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o767
+ }];
+
+ // Files for a complete successful update. This can be used for a complete
+ // failed update by calling setTestFilesAndDirsForFailure.
+var gTestFilesCompleteSuccess = [
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "precomplete",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_PARTIAL_PRECOMPLETE,
+ compareFile: FILE_COMPLETE_PRECOMPLETE,
+ originalPerms: 0o666,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginstext0",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginspng1.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginspng0.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "partial.png",
+ compareFile: "complete.png",
+ originalPerms: 0o666,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "removed-files",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_PARTIAL_REMOVEDFILES,
+ compareFile: FILE_COMPLETE_REMOVEDFILES,
+ originalPerms: 0o666,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "partial.png",
+ compareFile: "complete.png",
+ originalPerms: 0o666,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "exe0.exe",
+ relPathDir: DIR_MACOS,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_HELPER_BIN,
+ compareFile: FILE_COMPLETE_EXE,
+ originalPerms: 0o777,
+ comparePerms: 0o755
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "10text0",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "0exe0.exe",
+ relPathDir: DIR_RESOURCES + "0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_HELPER_BIN,
+ compareFile: FILE_COMPLETE_EXE,
+ originalPerms: 0o777,
+ comparePerms: 0o755
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "00text1",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o677,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "00text0",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "00png0.png",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: 0o776,
+ comparePerms: 0o644
+ }, {
+ description: "Removed by precomplete (remove)",
+ fileName: "20text0",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null
+ }, {
+ description: "Removed by precomplete (remove)",
+ fileName: "20png0.png",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null
+ }];
+
+// Concatenate the common files to the end of the array.
+gTestFilesCompleteSuccess = gTestFilesCompleteSuccess.concat(gTestFilesCommon);
+
+// Files for a partial successful update. This can be used for a partial failed
+// update by calling setTestFilesAndDirsForFailure.
+var gTestFilesPartialSuccess = [
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "precomplete",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_PRECOMPLETE,
+ compareFile: FILE_PARTIAL_PRECOMPLETE,
+ originalPerms: 0o666,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginstext0",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest if the file exists (patch-if)",
+ fileName: "searchpluginspng1.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ }, {
+ description: "Patched by update.manifest if the file exists (patch-if)",
+ fileName: "searchpluginspng0.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions1png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ }, {
+ description: "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions1png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ }, {
+ description: "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o644,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions0png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o644,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions0png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o644,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest (patch)",
+ fileName: "exe0.exe",
+ relPathDir: DIR_MACOS,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_EXE,
+ compareFile: FILE_PARTIAL_EXE,
+ originalPerms: 0o755,
+ comparePerms: 0o755
+ }, {
+ description: "Patched by update.manifest (patch)",
+ fileName: "0exe0.exe",
+ relPathDir: DIR_RESOURCES + "0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_EXE,
+ compareFile: FILE_PARTIAL_EXE,
+ originalPerms: 0o755,
+ comparePerms: 0o755
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "00text0",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o644,
+ comparePerms: 0o644
+ }, {
+ description: "Patched by update.manifest (patch)",
+ fileName: "00png0.png",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "20text0",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "20png0.png",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "partial.png",
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Added by update.manifest (add)",
+ fileName: "00text2",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644
+ }, {
+ description: "Removed by update.manifest (remove)",
+ fileName: "10text0",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null
+ }, {
+ description: "Removed by update.manifest (remove)",
+ fileName: "00text1",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null
+ }];
+
+// Concatenate the common files to the end of the array.
+gTestFilesPartialSuccess = gTestFilesPartialSuccess.concat(gTestFilesCommon);
+
+var gTestDirsCommon = [
+ {
+ relPathDir: DIR_RESOURCES + "3/",
+ dirRemoved: false,
+ files: ["3text0", "3text1"],
+ filesRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "4/",
+ dirRemoved: true,
+ files: ["4text0", "4text1"],
+ filesRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "5/",
+ dirRemoved: true,
+ files: ["5test.exe", "5text0", "5text1"],
+ filesRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "6/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "7/",
+ dirRemoved: true,
+ files: ["7text0", "7text1"],
+ subDirs: ["70/", "71/"],
+ subDirFiles: ["7xtest.exe", "7xtext0", "7xtext1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "8/",
+ dirRemoved: false
+ }, {
+ relPathDir: DIR_RESOURCES + "8/80/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "8/81/",
+ dirRemoved: false,
+ files: ["81text0", "81text1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "8/82/",
+ dirRemoved: false,
+ subDirs: ["820/", "821/"]
+ }, {
+ relPathDir: DIR_RESOURCES + "8/83/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "8/84/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "8/85/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "8/86/",
+ dirRemoved: true,
+ files: ["86text0", "86text1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "8/87/",
+ dirRemoved: true,
+ subDirs: ["870/", "871/"],
+ subDirFiles: ["87xtext0", "87xtext1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "8/88/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "8/89/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/90/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/91/",
+ dirRemoved: false,
+ files: ["91text0", "91text1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "9/92/",
+ dirRemoved: false,
+ subDirs: ["920/", "921/"]
+ }, {
+ relPathDir: DIR_RESOURCES + "9/93/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/94/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/95/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/96/",
+ dirRemoved: true,
+ files: ["96text0", "96text1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "9/97/",
+ dirRemoved: true,
+ subDirs: ["970/", "971/"],
+ subDirFiles: ["97xtext0", "97xtext1"]
+ }, {
+ relPathDir: DIR_RESOURCES + "9/98/",
+ dirRemoved: true
+ }, {
+ relPathDir: DIR_RESOURCES + "9/99/",
+ dirRemoved: true
+ }];
+
+// Directories for a complete successful update. This array can be used for a
+// complete failed update by calling setTestFilesAndDirsForFailure.
+var gTestDirsCompleteSuccess = [
+ {
+ description: "Removed by precomplete (rmdir)",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ dirRemoved: true
+ }, {
+ description: "Removed by precomplete (rmdir)",
+ relPathDir: DIR_RESOURCES + "2/",
+ dirRemoved: true
+ }];
+
+// Concatenate the common files to the beginning of the array.
+gTestDirsCompleteSuccess = gTestDirsCommon.concat(gTestDirsCompleteSuccess);
+
+// Directories for a partial successful update. This array can be used for a
+// partial failed update by calling setTestFilesAndDirsForFailure.
+var gTestDirsPartialSuccess = [
+ {
+ description: "Removed by update.manifest (rmdir)",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ dirRemoved: true
+ }, {
+ description: "Removed by update.manifest (rmdir)",
+ relPathDir: DIR_RESOURCES + "1/",
+ dirRemoved: true
+ }];
+
+// Concatenate the common files to the beginning of the array.
+gTestDirsPartialSuccess = gTestDirsCommon.concat(gTestDirsPartialSuccess);
+
+// This makes it possible to run most tests on xulrunner where the update
+// channel default preference is not set.
+if (MOZ_APP_NAME == "xulrunner") {
+ try {
+ gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ } catch (e) {
+ setUpdateChannel("test_channel");
+ }
+}
+
+/**
+ * Helper function for setting up the test environment.
+ */
+function setupTestCommon() {
+ debugDump("start - general test setup");
+
+ Assert.strictEqual(gTestID, undefined,
+ "gTestID should be 'undefined' (setupTestCommon should " +
+ "only be called once)");
+
+ let caller = Components.stack.caller;
+ gTestID = caller.filename.toString().split("/").pop().split(".")[0];
+
+ // Tests that don't work with XULRunner.
+ const XUL_RUNNER_INCOMPATIBLE = ["marAppApplyUpdateAppBinInUseStageSuccess_win",
+ "marAppApplyUpdateStageSuccess",
+ "marAppApplyUpdateSuccess",
+ "marAppApplyUpdateAppBinInUseStageSuccessSvc_win",
+ "marAppApplyUpdateStageSuccessSvc",
+ "marAppApplyUpdateSuccessSvc"];
+ // Replace with Array.prototype.includes when it has stabilized.
+ if (MOZ_APP_NAME == "xulrunner" &&
+ XUL_RUNNER_INCOMPATIBLE.indexOf(gTestID) != -1) {
+ logTestInfo("Unable to run this test on xulrunner");
+ return false;
+ }
+
+ if (IS_SERVICE_TEST && !shouldRunServiceTest()) {
+ return false;
+ }
+
+ do_test_pending();
+
+ setDefaultPrefs();
+
+ // Don't attempt to show a prompt when an update finishes.
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, true);
+
+ gGREDirOrig = getGREDir();
+ gGREBinDirOrig = getGREBinDir();
+ gAppDirOrig = getAppBaseDir();
+
+ let applyDir = getApplyDirFile(null, true).parent;
+
+ // Try to remove the directory used to apply updates and the updates directory
+ // on platforms other than Windows. Since the test hasn't ran yet and the
+ // directory shouldn't exist finished this is non-fatal for the test.
+ if (applyDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + applyDir.path);
+ try {
+ removeDirRecursive(applyDir);
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Path: " +
+ applyDir.path + ", Exception: " + e);
+ // When the application doesn't exit properly it can cause the test to
+ // fail again on the second run with an NS_ERROR_FILE_ACCESS_DENIED error
+ // along with no useful information in the test log. To prevent this use
+ // a different directory for the test when it isn't possible to remove the
+ // existing test directory (bug 1294196).
+ gTestID += "_new";
+ logTestInfo("using a new directory for the test by changing gTestID " +
+ "since there is an existing test directory that can't be " +
+ "removed, gTestID: " + gTestID);
+ }
+ }
+
+ if (IS_WIN) {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SERVICE_ENABLED,
+ IS_SERVICE_TEST ? true : false);
+ }
+
+ // adjustGeneralPaths registers a cleanup function that calls end_test when
+ // it is defined as a function.
+ adjustGeneralPaths();
+ // Logged once here instead of in the mock directory provider to lessen test
+ // log spam.
+ debugDump("Updates Directory (UpdRootD) Path: " + getMockUpdRootD().path);
+
+ // This prevents a warning about not being able to find the greprefs.js file
+ // from being logged.
+ let grePrefsFile = getGREDir();
+ if (!grePrefsFile.exists()) {
+ grePrefsFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+ grePrefsFile.append("greprefs.js");
+ if (!grePrefsFile.exists()) {
+ grePrefsFile.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+
+ // Remove the updates directory on Windows and Mac OS X which is located
+ // outside of the application directory after the call to adjustGeneralPaths
+ // has set it up. Since the test hasn't ran yet and the directory shouldn't
+ // exist this is non-fatal for the test.
+ if (IS_WIN || IS_MACOSX) {
+ let updatesDir = getMockUpdRootD();
+ if (updatesDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + updatesDir.path);
+ try {
+ removeDirRecursive(updatesDir);
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Path: " +
+ updatesDir.path + ", Exception: " + e);
+ }
+ }
+ }
+
+ debugDump("finish - general test setup");
+ return true;
+}
+
+/**
+ * Nulls out the most commonly used global vars used by tests to prevent leaks
+ * as needed and attempts to restore the system to its original state.
+ */
+function cleanupTestCommon() {
+ debugDump("start - general test cleanup");
+
+ // Force the update manager to reload the update data to prevent it from
+ // writing the old data to the files that have just been removed.
+ reloadUpdateManagerData();
+
+ if (gChannel) {
+ gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer);
+ }
+
+ // Call app update's observe method passing xpcom-shutdown to test that the
+ // shutdown of app update runs without throwing or leaking. The observer
+ // method is used directly instead of calling notifyObservers so components
+ // outside of the scope of this test don't assert and thereby cause app update
+ // tests to fail.
+ gAUS.observe(null, "xpcom-shutdown", "");
+
+ gTestserver = null;
+
+ if (IS_UNIX) {
+ // This will delete the launch script if it exists.
+ getLaunchScript();
+ }
+
+ if (IS_WIN && MOZ_APP_BASENAME) {
+ let appDir = getApplyDirFile(null, true);
+ let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla";
+ const REG_PATH = "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME +
+ "\\TaskBarIDs";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+ try {
+ key.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL);
+ if (key.hasValue(appDir.path)) {
+ key.removeValue(appDir.path);
+ }
+ } catch (e) {
+ }
+ try {
+ key.open(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL);
+ if (key.hasValue(appDir.path)) {
+ key.removeValue(appDir.path);
+ }
+ } catch (e) {
+ }
+ }
+
+ // The updates directory is located outside of the application directory and
+ // needs to be removed on Windows and Mac OS X.
+ if (IS_WIN || IS_MACOSX) {
+ let updatesDir = getMockUpdRootD();
+ // Try to remove the directory used to apply updates. Since the test has
+ // already finished this is non-fatal for the test.
+ if (updatesDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + updatesDir.path);
+ try {
+ removeDirRecursive(updatesDir);
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Path: " +
+ updatesDir.path + ", Exception: " + e);
+ }
+ if (IS_MACOSX) {
+ let updatesRootDir = gUpdatesRootDir.clone();
+ while (updatesRootDir.path != updatesDir.path) {
+ if (updatesDir.exists()) {
+ debugDump("attempting to remove directory. Path: " +
+ updatesDir.path);
+ try {
+ // Try to remove the directory without the recursive flag set
+ // since the top level directory has already had its contents
+ // removed and the parent directory might still be used by a
+ // different test.
+ updatesDir.remove(false);
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Path: " +
+ updatesDir.path + ", Exception: " + e);
+ if (e == Cr.NS_ERROR_FILE_DIR_NOT_EMPTY) {
+ break;
+ }
+ }
+ }
+ updatesDir = updatesDir.parent;
+ }
+ }
+ }
+ }
+
+ let applyDir = getApplyDirFile(null, true).parent;
+
+ // Try to remove the directory used to apply updates. Since the test has
+ // already finished this is non-fatal for the test.
+ if (applyDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + applyDir.path);
+ try {
+ removeDirRecursive(applyDir);
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Path: " +
+ applyDir.path + ", Exception: " + e);
+ }
+ }
+
+ resetEnvironment();
+
+ debugDump("finish - general test cleanup");
+}
+
+/**
+ * Helper function that calls do_test_finished that tracks whether a parallel
+ * run of a test passed when it runs synchronously so the log output can be
+ * inspected.
+ */
+function doTestFinish() {
+ if (DEBUG_AUS_TEST) {
+ // This prevents do_print errors from being printed by the xpcshell test
+ // harness due to nsUpdateService.js logging to the console when the
+ // app.update.log preference is true.
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false);
+ gAUS.observe(null, "nsPref:changed", PREF_APP_UPDATE_LOG);
+ }
+ do_execute_soon(do_test_finished);
+}
+
+/**
+ * Sets the most commonly used preferences used by tests
+ */
+function setDefaultPrefs() {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_ENABLED, true);
+ if (DEBUG_AUS_TEST) {
+ // Enable Update logging
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true);
+ } else {
+ // Some apps set this preference to true by default
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false);
+ }
+ // In case telemetry is enabled for xpcshell tests.
+ Services.prefs.setBoolPref(PREF_TOOLKIT_TELEMETRY_ENABLED, false);
+}
+
+/**
+ * Helper function for updater binary tests that sets the appropriate values
+ * to check for update failures.
+ */
+function setTestFilesAndDirsForFailure() {
+ gTestFiles.forEach(function STFADFF_Files(aTestFile) {
+ aTestFile.compareContents = aTestFile.originalContents;
+ aTestFile.compareFile = aTestFile.originalFile;
+ aTestFile.comparePerms = aTestFile.originalPerms;
+ });
+
+ gTestDirs.forEach(function STFADFF_Dirs(aTestDir) {
+ aTestDir.dirRemoved = false;
+ if (aTestDir.filesRemoved) {
+ aTestDir.filesRemoved = false;
+ }
+ });
+}
+
+/**
+ * Helper function for updater binary tests that prevents the distribution
+ * directory files from being created.
+ */
+function preventDistributionFiles() {
+ gTestFiles = gTestFiles.filter(function(aTestFile) {
+ return aTestFile.relPathDir.indexOf("distribution/") == -1;
+ });
+
+ gTestDirs = gTestDirs.filter(function(aTestDir) {
+ return aTestDir.relPathDir.indexOf("distribution/") == -1;
+ });
+}
+
+/**
+ * On Mac OS X this sets the last modified time for the app bundle directory to
+ * a date in the past to test that the last modified time is updated when an
+ * update has been successfully applied (bug 600098).
+ */
+function setAppBundleModTime() {
+ if (!IS_MACOSX) {
+ return;
+ }
+ let now = Date.now();
+ let yesterday = now - (1000 * 60 * 60 * 24);
+ let applyToDir = getApplyDirFile();
+ applyToDir.lastModifiedTime = yesterday;
+}
+
+/**
+ * On Mac OS X this checks that the last modified time for the app bundle
+ * directory has been updated when an update has been successfully applied
+ * (bug 600098).
+ */
+function checkAppBundleModTime() {
+ if (!IS_MACOSX) {
+ return;
+ }
+ let now = Date.now();
+ let applyToDir = getApplyDirFile();
+ let timeDiff = Math.abs(applyToDir.lastModifiedTime - now);
+ Assert.ok(timeDiff < MAC_MAX_TIME_DIFFERENCE,
+ "the last modified time on the apply to directory should " +
+ "change after a successful update");
+}
+
+/**
+ * On Mac OS X and Windows this checks if the post update '.running' file exists
+ * to determine if the post update binary was launched.
+ *
+ * @param aShouldExist
+ * Whether the post update '.running' file should exist.
+ */
+function checkPostUpdateRunningFile(aShouldExist) {
+ if (!IS_WIN && !IS_MACOSX) {
+ return;
+ }
+ let postUpdateRunningFile = getPostUpdateFile(".running");
+ if (aShouldExist) {
+ Assert.ok(postUpdateRunningFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(postUpdateRunningFile.path));
+ } else {
+ Assert.ok(!postUpdateRunningFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(postUpdateRunningFile.path));
+ }
+}
+
+/**
+ * Initializes the most commonly used settings and creates an instance of the
+ * update service stub.
+ */
+function standardInit() {
+ createAppInfo("xpcshell@tests.mozilla.org", APP_INFO_NAME, "1.0", "2.0");
+ // Initialize the update service stub component
+ initUpdateServiceStub();
+}
+
+/**
+ * Helper function for getting the application version from the application.ini
+ * file. This will look in both the GRE and the application directories for the
+ * application.ini file.
+ *
+ * @return The version string from the application.ini file.
+ */
+function getAppVersion() {
+ // Read the application.ini and use its application version.
+ let iniFile = gGREDirOrig.clone();
+ iniFile.append(FILE_APPLICATION_INI);
+ if (!iniFile.exists()) {
+ iniFile = gGREBinDirOrig.clone();
+ iniFile.append(FILE_APPLICATION_INI);
+ }
+ Assert.ok(iniFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(iniFile.path));
+ let iniParser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"].
+ getService(Ci.nsIINIParserFactory).
+ createINIParser(iniFile);
+ return iniParser.getString("App", "Version");
+}
+
+/**
+ * Helper function for getting the relative path to the directory where the
+ * application binary is located (e.g. <test_file_leafname>/dir.app/).
+ *
+ * Note: The dir.app subdirectory under <test_file_leafname> is needed for
+ * platforms other than Mac OS X so the tests can run in parallel due to
+ * update staging creating a lock file named moz_update_in_progress.lock in
+ * the parent directory of the installation directory.
+ *
+ * @return The relative path to the directory where application binary is
+ * located.
+ */
+function getApplyDirPath() {
+ return gTestID + "/dir.app/";
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the directory where the
+ * update will be applied.
+ *
+ * The files for the update are located two directories below the apply to
+ * directory since Mac OS X sets the last modified time for the root directory
+ * to the current time and if the update changes any files in the root directory
+ * then it wouldn't be possible to test (bug 600098).
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the test's directory. If not specified the test's directory will be
+ * returned.
+ * @param aAllowNonexistent (optional)
+ * Whether the file must exist. If false or not specified the file must
+ * exist or the function will throw.
+ * @return The nsIFile for the file in the directory where the update will be
+ * applied.
+ * @throws If aAllowNonexistent is not specified or is false and the file or
+ * directory does not exist.
+ */
+function getApplyDirFile(aRelPath, aAllowNonexistent) {
+ let relpath = getApplyDirPath() + (aRelPath ? aRelPath : "");
+ return do_get_file(relpath, aAllowNonexistent);
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the directory where the
+ * update will be staged.
+ *
+ * The files for the update are located two directories below the stage
+ * directory since Mac OS X sets the last modified time for the root directory
+ * to the current time and if the update changes any files in the root directory
+ * then it wouldn't be possible to test (bug 600098).
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the stage directory. If not specified the stage directory will be
+ * returned.
+ * @param aAllowNonexistent (optional)
+ * Whether the file must exist. If false or not specified the file must
+ * exist or the function will throw.
+ * @return The nsIFile for the file in the directory where the update will be
+ * staged.
+ * @throws If aAllowNonexistent is not specified or is false and the file or
+ * directory does not exist.
+ */
+function getStageDirFile(aRelPath, aAllowNonexistent) {
+ if (IS_MACOSX) {
+ let file = getMockUpdRootD();
+ file.append(DIR_UPDATES);
+ file.append(DIR_PATCH);
+ file.append(DIR_UPDATED);
+ if (aRelPath) {
+ let pathParts = aRelPath.split("/");
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ file.append(pathParts[i]);
+ }
+ }
+ }
+ if (!aAllowNonexistent) {
+ Assert.ok(file.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(file.path));
+ }
+ return file;
+ }
+
+ let relpath = getApplyDirPath() + DIR_UPDATED + "/" + (aRelPath ? aRelPath : "");
+ return do_get_file(relpath, aAllowNonexistent);
+}
+
+/**
+ * Helper function for getting the relative path to the directory where the
+ * test data files are located.
+ *
+ * @return The relative path to the directory where the test data files are
+ * located.
+ */
+function getTestDirPath() {
+ return "../data/";
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the test data
+ * directory.
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the test's data directory. If not specified the test's data
+ * directory will be returned.
+ * @param aAllowNonExists (optional)
+ * Whether or not to throw an error if the path exists.
+ * If not specified, then false is used.
+ * @return The nsIFile for the file in the test data directory.
+ * @throws If the file or directory does not exist.
+ */
+function getTestDirFile(aRelPath, aAllowNonExists) {
+ let relpath = getTestDirPath() + (aRelPath ? aRelPath : "");
+ return do_get_file(relpath, !!aAllowNonExists);
+}
+
+/**
+ * Helper function for getting the nsIFile for the maintenance service
+ * directory on Windows.
+ *
+ * @return The nsIFile for the maintenance service directory.
+ */
+function getMaintSvcDir() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_PROGRAM_FILES = 0x26;
+ const CSIDL_PROGRAM_FILESX86 = 0x2A;
+ // This will return an empty string on our Win XP build systems.
+ let maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILESX86);
+ if (maintSvcDir) {
+ maintSvcDir.append("Mozilla Maintenance Service");
+ debugDump("using CSIDL_PROGRAM_FILESX86 - maintenance service install " +
+ "directory path: " + maintSvcDir.path);
+ }
+ if (!maintSvcDir || !maintSvcDir.exists()) {
+ maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILES);
+ if (maintSvcDir) {
+ maintSvcDir.append("Mozilla Maintenance Service");
+ debugDump("using CSIDL_PROGRAM_FILES - maintenance service install " +
+ "directory path: " + maintSvcDir.path);
+ }
+ }
+ if (!maintSvcDir) {
+ do_throw("Unable to find the maintenance service install directory");
+ }
+
+ return maintSvcDir;
+}
+
+/**
+ * Get the nsILocalFile for a Windows special folder determined by the CSIDL
+ * passed.
+ *
+ * @param aCSIDL
+ * The CSIDL for the Windows special folder.
+ * @return The nsILocalFile for the Windows special folder.
+ * @throws If called from a platform other than Windows.
+ */
+function getSpecialFolderDir(aCSIDL) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let lib = ctypes.open("shell32");
+ let SHGetSpecialFolderPath = lib.declare("SHGetSpecialFolderPathW",
+ ctypes.winapi_abi,
+ ctypes.bool, /* bool(return) */
+ ctypes.int32_t, /* HWND hwndOwner */
+ ctypes.char16_t.ptr, /* LPTSTR lpszPath */
+ ctypes.int32_t, /* int csidl */
+ ctypes.bool /* BOOL fCreate */);
+
+ let aryPath = ctypes.char16_t.array()(260);
+ let rv = SHGetSpecialFolderPath(0, aryPath, aCSIDL, false);
+ lib.close();
+
+ let path = aryPath.readString(); // Convert the c-string to js-string
+ if (!path) {
+ return null;
+ }
+ debugDump("SHGetSpecialFolderPath returned path: " + path);
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ dir.initWithPath(path);
+ return dir;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gInstallDirPathHash", function test_gIDPH() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ // Figure out where we should check for a cached hash value
+ if (!MOZ_APP_BASENAME) {
+ return null;
+ }
+
+ let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla";
+ let appDir = getApplyDirFile(null, true);
+
+ const REG_PATH = "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME +
+ "\\TaskBarIDs";
+ let regKey = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+ try {
+ regKey.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL);
+ regKey.writeStringValue(appDir.path, gTestID);
+ return gTestID;
+ } catch (e) {
+ }
+
+ try {
+ regKey.create(Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER, REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL);
+ regKey.writeStringValue(appDir.path, gTestID);
+ return gTestID;
+ } catch (e) {
+ logTestInfo("failed to create registry key. Registry Path: " + REG_PATH +
+ ", Key Name: " + appDir.path + ", Key Value: " + gTestID +
+ ", Exception " + e);
+ }
+ return null;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gLocalAppDataDir", function test_gLADD() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_LOCAL_APPDATA = 0x1c;
+ return getSpecialFolderDir(CSIDL_LOCAL_APPDATA);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gProgFilesDir", function test_gPFD() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_PROGRAM_FILES = 0x26;
+ return getSpecialFolderDir(CSIDL_PROGRAM_FILES);
+});
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ */
+function getMockUpdRootD() {
+ if (IS_WIN) {
+ return getMockUpdRootDWin();
+ }
+
+ if (IS_MACOSX) {
+ return getMockUpdRootDMac();
+ }
+
+ return getApplyDirFile(DIR_MACOS, true);
+}
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ */
+function getMockUpdRootDWin() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let localAppDataDir = gLocalAppDataDir.clone();
+ let progFilesDir = gProgFilesDir.clone();
+ let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent;
+
+ let appDirPath = appDir.path;
+ let relPathUpdates = "";
+ if (gInstallDirPathHash && (MOZ_APP_VENDOR || MOZ_APP_BASENAME)) {
+ relPathUpdates += (MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME) +
+ "\\" + DIR_UPDATES + "\\" + gInstallDirPathHash;
+ }
+
+ if (!relPathUpdates && progFilesDir) {
+ if (appDirPath.length > progFilesDir.path.length) {
+ if (appDirPath.substr(0, progFilesDir.path.length) == progFilesDir.path) {
+ if (MOZ_APP_VENDOR && MOZ_APP_BASENAME) {
+ relPathUpdates += MOZ_APP_VENDOR + "\\" + MOZ_APP_BASENAME;
+ } else {
+ relPathUpdates += MOZ_APP_BASENAME;
+ }
+ relPathUpdates += appDirPath.substr(progFilesDir.path.length);
+ }
+ }
+ }
+
+ if (!relPathUpdates) {
+ if (MOZ_APP_VENDOR && MOZ_APP_BASENAME) {
+ relPathUpdates += MOZ_APP_VENDOR + "\\" + MOZ_APP_BASENAME;
+ } else {
+ relPathUpdates += MOZ_APP_BASENAME;
+ }
+ relPathUpdates += "\\" + MOZ_APP_NAME;
+ }
+
+ let updatesDir = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ updatesDir.initWithPath(localAppDataDir.path + "\\" + relPathUpdates);
+ return updatesDir;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gUpdatesRootDir", function test_gURD() {
+ if (!IS_MACOSX) {
+ do_throw("Mac OS X only function called by a different platform!");
+ }
+
+ let dir = Services.dirsvc.get("ULibDir", Ci.nsILocalFile);
+ dir.append("Caches");
+ if (MOZ_APP_VENDOR || MOZ_APP_BASENAME) {
+ dir.append(MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME);
+ } else {
+ dir.append("Mozilla");
+ }
+ dir.append(DIR_UPDATES);
+ return dir;
+});
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ */
+function getMockUpdRootDMac() {
+ if (!IS_MACOSX) {
+ do_throw("Mac OS X only function called by a different platform!");
+ }
+
+ let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).
+ parent.parent.parent;
+ let appDirPath = appDir.path;
+ appDirPath = appDirPath.substr(0, appDirPath.length - 4);
+
+ let pathUpdates = gUpdatesRootDir.path + appDirPath;
+ let updatesDir = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ updatesDir.initWithPath(pathUpdates);
+ return updatesDir;
+}
+
+/**
+ * Creates an update in progress lock file in the specified directory on
+ * Windows.
+ *
+ * @param aDir
+ * The nsIFile for the directory where the lock file should be created.
+ */
+function createUpdateInProgressLockFile(aDir) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_UPDATE_IN_PROGRESS_LOCK);
+ file.create(file.NORMAL_FILE_TYPE, 0o444);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ file.fileAttributesWin |= file.WFA_READONLY;
+ file.fileAttributesWin &= ~file.WFA_READWRITE;
+ Assert.ok(file.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(file.path));
+ Assert.ok(!file.isWritable(),
+ "the lock file should not be writeable");
+}
+
+/**
+ * Removes an update in progress lock file in the specified directory on
+ * Windows.
+ *
+ * @param aDir
+ * The nsIFile for the directory where the lock file is located.
+ */
+function removeUpdateInProgressLockFile(aDir) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_UPDATE_IN_PROGRESS_LOCK);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ file.fileAttributesWin |= file.WFA_READWRITE;
+ file.fileAttributesWin &= ~file.WFA_READONLY;
+ file.remove(false);
+ Assert.ok(!file.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(file.path));
+}
+
+/**
+ * Gets the test updater from the test data direcory.
+ *
+ * @return nsIFIle for the test updater.
+ */
+function getTestUpdater() {
+ let updater = getTestDirFile("updater.app", true);
+ if (!updater.exists()) {
+ updater = getTestDirFile(FILE_UPDATER_BIN);
+ if (!updater.exists()) {
+ do_throw("Unable to find the updater binary!");
+ }
+ }
+ Assert.ok(updater.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updater.path));
+ return updater;
+}
+
+/**
+ * Copies the test updater to the GRE binary directory and returns the nsIFile
+ * for the copied test updater.
+ *
+ * @return nsIFIle for the copied test updater.
+ */
+function copyTestUpdaterToBinDir() {
+ let testUpdater = getTestUpdater();
+ let updater = getGREBinDir();
+ updater.append(testUpdater.leafName);
+ if (!updater.exists()) {
+ testUpdater.copyToFollowingLinks(updater.parent, updater.leafName);
+ }
+ return updater;
+}
+
+/**
+ * Copies the test updater to the location where it will be launched to apply an
+ * update and returns the nsIFile for the copied test updater.
+ *
+ * @return nsIFIle for the copied test updater.
+ */
+function copyTestUpdaterForRunUsingUpdater() {
+ if (IS_WIN) {
+ return copyTestUpdaterToBinDir();
+ }
+
+ let testUpdater = getTestUpdater();
+ let updater = getUpdatesPatchDir();
+ updater.append(testUpdater.leafName);
+ if (!updater.exists()) {
+ testUpdater.copyToFollowingLinks(updater.parent, updater.leafName);
+ }
+
+ if (IS_MACOSX) {
+ updater.append("Contents");
+ updater.append("MacOS");
+ updater.append("org.mozilla.updater");
+ }
+ return updater;
+}
+
+/**
+ * Logs the contents of an update log and for maintenance service tests this
+ * will log the contents of the latest maintenanceservice.log.
+ *
+ * @param aLogLeafName
+ * The leaf name of the update log.
+ */
+function logUpdateLog(aLogLeafName) {
+ let updateLog = getUpdateLog(aLogLeafName);
+ if (updateLog.exists()) {
+ // xpcshell tests won't display the entire contents so log each line.
+ let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
+ updateLogContents = replaceLogPaths(updateLogContents);
+ let aryLogContents = updateLogContents.split("\n");
+ logTestInfo("contents of " + updateLog.path + ":");
+ aryLogContents.forEach(function RU_LC_FE(aLine) {
+ logTestInfo(aLine);
+ });
+ } else {
+ logTestInfo("update log doesn't exist, path: " + updateLog.path);
+ }
+
+ if (IS_SERVICE_TEST) {
+ let serviceLog = getMaintSvcDir();
+ serviceLog.append("logs");
+ serviceLog.append("maintenanceservice.log");
+ if (serviceLog.exists()) {
+ // xpcshell tests won't display the entire contents so log each line.
+ let serviceLogContents = readFileBytes(serviceLog).replace(/\r\n/g, "\n");
+ serviceLogContents = replaceLogPaths(serviceLogContents);
+ let aryLogContents = serviceLogContents.split("\n");
+ logTestInfo("contents of " + serviceLog.path + ":");
+ aryLogContents.forEach(function RU_LC_FE(aLine) {
+ logTestInfo(aLine);
+ });
+ } else {
+ logTestInfo("maintenance service log doesn't exist, path: " +
+ serviceLog.path);
+ }
+ }
+}
+
+/**
+ * Gets the maintenance service log contents.
+ */
+function readServiceLogFile() {
+ let file = getMaintSvcDir();
+ file.append("logs");
+ file.append("maintenanceservice.log");
+ return readFile(file);
+}
+
+/**
+ * Launches the updater binary to apply an update for updater tests.
+ *
+ * @param aExpectedStatus
+ * The expected value of update.status when the test finishes. For
+ * service tests passing STATE_PENDING or STATE_APPLIED will change the
+ * value to STATE_PENDING_SVC and STATE_APPLIED_SVC respectively.
+ * @param aSwitchApp
+ * If true the update should switch the application with an updated
+ * staged application and if false the update should be applied to the
+ * installed application.
+ * @param aExpectedExitValue
+ * The expected exit value from the updater binary for non-service
+ * tests.
+ * @param aCheckSvcLog
+ * Whether the service log should be checked for service tests.
+ * @param aPatchDirPath (optional)
+ * When specified the patch directory path to use for invalid argument
+ * tests otherwise the normal path will be used.
+ * @param aInstallDirPath (optional)
+ * When specified the install directory path to use for invalid
+ * argument tests otherwise the normal path will be used.
+ * @param aApplyToDirPath (optional)
+ * When specified the apply to / working directory path to use for
+ * invalid argument tests otherwise the normal path will be used.
+ * @param aCallbackPath (optional)
+ * When specified the callback path to use for invalid argument tests
+ * otherwise the normal path will be used.
+ */
+function runUpdate(aExpectedStatus, aSwitchApp, aExpectedExitValue, aCheckSvcLog,
+ aPatchDirPath, aInstallDirPath, aApplyToDirPath,
+ aCallbackPath) {
+ let isInvalidArgTest = !!aPatchDirPath || !!aInstallDirPath ||
+ !!aApplyToDirPath || aCallbackPath;
+
+ let svcOriginalLog;
+ if (IS_SERVICE_TEST) {
+ copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_BIN, false);
+ copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_INSTALLER_BIN, false);
+ if (aCheckSvcLog) {
+ svcOriginalLog = readServiceLogFile();
+ }
+ }
+
+ // Copy the updater binary to the directory where it will apply updates.
+ let updateBin = copyTestUpdaterForRunUsingUpdater();
+ Assert.ok(updateBin.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updateBin.path));
+
+ let updatesDirPath = aPatchDirPath || getUpdatesPatchDir().path;
+ let installDirPath = aInstallDirPath || getApplyDirFile(null, true).path;
+ let applyToDirPath = aApplyToDirPath || getApplyDirFile(null, true).path;
+ let stageDirPath = aApplyToDirPath || getStageDirFile(null, true).path;
+
+ let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile);
+ callbackApp.permissions = PERMS_DIRECTORY;
+
+ setAppBundleModTime();
+
+ let args = [updatesDirPath, installDirPath];
+ if (aSwitchApp) {
+ args[2] = stageDirPath;
+ args[3] = "0/replace";
+ } else {
+ args[2] = applyToDirPath;
+ args[3] = "0";
+ }
+
+ let launchBin = IS_SERVICE_TEST && isInvalidArgTest ? callbackApp : updateBin;
+
+ if (!isInvalidArgTest) {
+ args = args.concat([callbackApp.parent.path, callbackApp.path]);
+ args = args.concat(gCallbackArgs);
+ } else if (IS_SERVICE_TEST) {
+ args = ["launch-service", updateBin.path].concat(args);
+ } else if (aCallbackPath) {
+ args = args.concat([callbackApp.parent.path, aCallbackPath]);
+ }
+
+ debugDump("launching the program: " + launchBin.path + " " + args.join(" "));
+
+ if (aSwitchApp && !isInvalidArgTest) {
+ // We want to set the env vars again
+ gShouldResetEnv = undefined;
+ }
+
+ setEnvironment();
+
+ let process = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ process.init(launchBin);
+ process.run(true, args, args.length);
+
+ resetEnvironment();
+
+ let status = readStatusFile();
+ if ((!IS_SERVICE_TEST && process.exitValue != aExpectedExitValue) ||
+ status != aExpectedStatus) {
+ if (process.exitValue != aExpectedExitValue) {
+ logTestInfo("updater exited with unexpected value! Got: " +
+ process.exitValue + ", Expected: " + aExpectedExitValue);
+ }
+ if (status != aExpectedStatus) {
+ logTestInfo("update status is not the expected status! Got: " + status +
+ ", Expected: " + aExpectedStatus);
+ }
+ logUpdateLog(FILE_LAST_UPDATE_LOG);
+ }
+
+ if (!IS_SERVICE_TEST) {
+ Assert.equal(process.exitValue, aExpectedExitValue,
+ "the process exit value" + MSG_SHOULD_EQUAL);
+ }
+ Assert.equal(status, aExpectedStatus,
+ "the update status" + MSG_SHOULD_EQUAL);
+
+ if (IS_SERVICE_TEST && aCheckSvcLog) {
+ let contents = readServiceLogFile();
+ Assert.notEqual(contents, svcOriginalLog,
+ "the contents of the maintenanceservice.log should not " +
+ "be the same as the original contents");
+ if (!isInvalidArgTest) {
+ Assert.notEqual(contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), -1,
+ "the contents of the maintenanceservice.log should " +
+ "contain the successful launch string");
+ }
+ }
+
+ do_execute_soon(runUpdateFinished);
+}
+
+/**
+ * Launches the helper binary synchronously with the specified arguments for
+ * updater tests.
+ *
+ * @param aArgs
+ * The arguments to pass to the helper binary.
+ * @return the process exit value returned by the helper binary.
+ */
+function runTestHelperSync(aArgs) {
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let process = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ process.init(helperBin);
+ debugDump("Running " + helperBin.path + " " + aArgs.join(" "));
+ process.run(true, aArgs, aArgs.length);
+ return process.exitValue;
+}
+
+/**
+ * Creates a symlink for updater tests.
+ */
+function createSymlink() {
+ let args = ["setup-symlink", "moz-foo", "moz-bar", "target",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link"];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the helper process exit value should be 0");
+ getApplyDirFile(DIR_RESOURCES + "link", false).permissions = 0o666;
+ args = ["setup-symlink", "moz-foo2", "moz-bar2", "target2",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link2", "change-perm"];
+ exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the helper process exit value should be 0");
+}
+
+/**
+ * Removes a symlink for updater tests.
+ */
+function removeSymlink() {
+ let args = ["remove-symlink", "moz-foo", "moz-bar", "target",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link"];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the helper process exit value should be 0");
+ args = ["remove-symlink", "moz-foo2", "moz-bar2", "target2",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link2"];
+ exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the helper process exit value should be 0");
+}
+
+/**
+ * Checks a symlink for updater tests.
+ */
+function checkSymlink() {
+ let args = ["check-symlink",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link"];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the helper process exit value should be 0");
+}
+
+/**
+ * Sets the active update and related information for updater tests.
+ */
+function setupActiveUpdate() {
+ let state = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+ let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ let patches = getLocalPatchString(null, null, null, null, null, "true",
+ state);
+ let updates = getLocalUpdateString(patches, null, null, null, null, null,
+ null, null, null, null, "true", channel);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeVersionFile(DEFAULT_UPDATE_VERSION);
+ writeStatusFile(state);
+ reloadUpdateManagerData();
+ Assert.ok(!!gUpdateManager.activeUpdate,
+ "the active update should be defined");
+}
+
+/**
+ * Gets the specified update log.
+ *
+ * @param aLogLeafName
+ * The leaf name of the log to get.
+ * @return nsIFile for the update log.
+ */
+function getUpdateLog(aLogLeafName) {
+ let updateLog = getUpdatesDir();
+ if (aLogLeafName == FILE_UPDATE_LOG) {
+ updateLog.append(DIR_PATCH);
+ }
+ updateLog.append(aLogLeafName);
+ return updateLog;
+}
+
+/**
+ * The update-staged observer for the call to nsIUpdateProcessor:processUpdate.
+ */
+const gUpdateStagedObserver = {
+ observe: function(aSubject, aTopic, aData) {
+ debugDump("observe called with topic: " + aTopic + ", data: " + aData);
+ if (aTopic == "update-staged") {
+ Services.obs.removeObserver(gUpdateStagedObserver, "update-staged");
+ // The environment is reset after the update-staged observer topic because
+ // processUpdate in nsIUpdateProcessor uses a new thread and clearing the
+ // environment immediately after calling processUpdate can clear the
+ // environment before the updater is launched.
+ resetEnvironment();
+ // Use do_execute_soon to prevent any failures from propagating to the
+ // update service.
+ do_execute_soon(checkUpdateStagedState.bind(null, aData));
+ }
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+};
+
+/**
+ * Stages an update using nsIUpdateProcessor:processUpdate for updater tests.
+ *
+ * @param aCheckSvcLog
+ * Whether the service log should be checked for service tests.
+ */
+function stageUpdate(aCheckSvcLog) {
+ debugDump("start - attempting to stage update");
+
+ if (IS_SERVICE_TEST && aCheckSvcLog) {
+ gSvcOriginalLogContents = readServiceLogFile();
+ }
+
+ Services.obs.addObserver(gUpdateStagedObserver, "update-staged", false);
+
+ setAppBundleModTime();
+ setEnvironment();
+ // Stage the update.
+ Cc["@mozilla.org/updates/update-processor;1"].
+ createInstance(Ci.nsIUpdateProcessor).
+ processUpdate(gUpdateManager.activeUpdate);
+
+ // The environment is not reset here because processUpdate in
+ // nsIUpdateProcessor uses a new thread and clearing the environment
+ // immediately after calling processUpdate can clear the environment before
+ // the updater is launched. Instead it is reset after the update-staged
+ // observer topic.
+
+ debugDump("finish - attempting to stage update");
+}
+
+/**
+ * Checks that the update state is correct as well as the expected files are
+ * present after staging and update for updater tests and then calls
+ * stageUpdateFinished.
+ *
+ * @param aUpdateState
+ * The update state received by the observer notification.
+ */
+function checkUpdateStagedState(aUpdateState) {
+ if (IS_WIN) {
+ if (IS_SERVICE_TEST) {
+ waitForServiceStop(false);
+ } else {
+ let updater = getApplyDirFile(FILE_UPDATER_BIN, true);
+ if (isFileInUse(updater)) {
+ do_timeout(FILE_IN_USE_TIMEOUT_MS,
+ checkUpdateStagedState.bind(null, aUpdateState));
+ return;
+ }
+ }
+ }
+
+ Assert.equal(aUpdateState, STATE_AFTER_STAGE,
+ "the notified state" + MSG_SHOULD_EQUAL);
+
+ if (!gStagingRemovedUpdate) {
+ Assert.equal(readStatusState(), STATE_AFTER_STAGE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+
+ Assert.equal(gUpdateManager.activeUpdate.state, STATE_AFTER_STAGE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ }
+
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_STAGE,
+ "the update state" + MSG_SHOULD_EQUAL);
+
+ let log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ let stageDir = getStageDirFile(null, true);
+ if (STATE_AFTER_STAGE == STATE_APPLIED ||
+ STATE_AFTER_STAGE == STATE_APPLIED_SVC) {
+ Assert.ok(stageDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(stageDir.path));
+ } else {
+ Assert.ok(!stageDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path));
+ }
+
+ if (IS_SERVICE_TEST && gSvcOriginalLogContents !== undefined) {
+ let contents = readServiceLogFile();
+ Assert.notEqual(contents, gSvcOriginalLogContents,
+ "the contents of the maintenanceservice.log should not " +
+ "be the same as the original contents");
+ Assert.notEqual(contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH), -1,
+ "the contents of the maintenanceservice.log should " +
+ "contain the successful launch string");
+ }
+
+ do_execute_soon(stageUpdateFinished);
+}
+
+/**
+ * Helper function to check whether the maintenance service updater tests should
+ * run. See bug 711660 for more details.
+ *
+ * @return true if the test should run and false if it shouldn't.
+ */
+function shouldRunServiceTest() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let binDir = getGREBinDir();
+ let updaterBin = binDir.clone();
+ updaterBin.append(FILE_UPDATER_BIN);
+ Assert.ok(updaterBin.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + updaterBin.leafName);
+
+ let updaterBinPath = updaterBin.path;
+ if (/ /.test(updaterBinPath)) {
+ updaterBinPath = '"' + updaterBinPath + '"';
+ }
+
+ let isBinSigned = isBinarySigned(updaterBinPath);
+
+ const REG_PATH = "SOFTWARE\\Mozilla\\MaintenanceService\\" +
+ "3932ecacee736d366d6436db0f55bce4";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].
+ createInstance(Ci.nsIWindowsRegKey);
+ try {
+ key.open(Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE, REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_READ | key.WOW64_64);
+ } catch (e) {
+ // The build system could sign the files and not have the test registry key
+ // in which case we should fail the test if the updater binary is signed so
+ // the build system can be fixed by adding the registry key.
+ if (IS_AUTHENTICODE_CHECK_ENABLED) {
+ Assert.ok(!isBinSigned,
+ "the updater.exe binary should not be signed when the test " +
+ "registry key doesn't exist (if it is, build system " +
+ "configuration bug?)");
+ }
+
+ logTestInfo("this test can only run on the buildbot build system at this " +
+ "time");
+ return false;
+ }
+
+ // Check to make sure the service is installed
+ let args = ["wait-for-service-stop", "MozillaMaintenance", "10"];
+ let exitValue = runTestHelperSync(args);
+ Assert.notEqual(exitValue, 0xEE, "the maintenance service should be " +
+ "installed (if not, build system configuration bug?)");
+
+ if (IS_AUTHENTICODE_CHECK_ENABLED) {
+ // The test registry key exists and IS_AUTHENTICODE_CHECK_ENABLED is true
+ // so the binaries should be signed. To run the test locally
+ // DISABLE_UPDATER_AUTHENTICODE_CHECK can be defined.
+ Assert.ok(isBinSigned,
+ "the updater.exe binary should be signed (if not, build system " +
+ "configuration bug?)");
+ }
+
+ // In case the machine is running an old maintenance service or if it
+ // is not installed, and permissions exist to install it. Then install
+ // the newer bin that we have since all of the other checks passed.
+ return attemptServiceInstall();
+}
+
+/**
+ * Helper function to check whether the a binary is signed.
+ *
+ * @param aBinPath
+ * The path to the file to check if it is signed.
+ * @return true if the file is signed and false if it isn't.
+ */
+function isBinarySigned(aBinPath) {
+ let args = ["check-signature", aBinPath];
+ let exitValue = runTestHelperSync(args);
+ if (exitValue != 0) {
+ logTestInfo("binary is not signed. " + FILE_HELPER_BIN + " returned " +
+ exitValue + " for file " + aBinPath);
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Helper function for asynchronously setting up the application files required
+ * to launch the application for the updater tests by either copying or creating
+ * symlinks for the files. This is needed for Windows debug builds which can
+ * lock a file that is being copied so that the tests can run in parallel. After
+ * the files have been copied the setupUpdaterTestFinished function will be
+ * called.
+ */
+function setupAppFilesAsync() {
+ gTimeoutRuns++;
+ try {
+ setupAppFiles();
+ } catch (e) {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while trying to setup application " +
+ "files! Exception: " + e);
+ }
+ do_execute_soon(setupAppFilesAsync);
+ return;
+ }
+
+ do_execute_soon(setupUpdaterTestFinished);
+}
+
+/**
+ * Helper function for setting up the application files required to launch the
+ * application for the updater tests by either copying or creating symlinks to
+ * the files.
+ */
+function setupAppFiles() {
+ debugDump("start - copying or creating symlinks to application files " +
+ "for the test");
+
+ let destDir = getApplyDirFile(null, true);
+ if (!destDir.exists()) {
+ try {
+ destDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ } catch (e) {
+ logTestInfo("unable to create directory! Path: " + destDir.path +
+ ", Exception: " + e);
+ do_throw(e);
+ }
+ }
+
+ // Required files for the application or the test that aren't listed in the
+ // dependentlibs.list file.
+ let appFiles = [{relPath: FILE_APP_BIN,
+ inGreDir: false},
+ {relPath: FILE_APPLICATION_INI,
+ inGreDir: true},
+ {relPath: "dependentlibs.list",
+ inGreDir: true}];
+
+ // On Linux the updater.png must also be copied
+ if (IS_UNIX && !IS_MACOSX) {
+ appFiles.push({relPath: "icons/updater.png",
+ inGreDir: true});
+ }
+
+ // Read the dependent libs file leafnames from the dependentlibs.list file
+ // into the array.
+ let deplibsFile = gGREDirOrig.clone();
+ deplibsFile.append("dependentlibs.list");
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ fis.init(deplibsFile, 0x01, 0o444, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ fis.QueryInterface(Ci.nsILineInputStream);
+
+ let hasMore;
+ let line = {};
+ do {
+ hasMore = fis.readLine(line);
+ appFiles.push({relPath: line.value,
+ inGreDir: false});
+ } while (hasMore);
+
+ fis.close();
+
+ appFiles.forEach(function CMAF_FLN_FE(aAppFile) {
+ copyFileToTestAppDir(aAppFile.relPath, aAppFile.inGreDir);
+ });
+
+ copyTestUpdaterToBinDir();
+
+ debugDump("finish - copying or creating symlinks to application files " +
+ "for the test");
+}
+
+/**
+ * Copies the specified files from the dist/bin directory into the test's
+ * application directory.
+ *
+ * @param aFileRelPath
+ * The relative path to the source and the destination of the file to
+ * copy.
+ * @param aInGreDir
+ * Whether the file is located in the GRE directory which is
+ * <bundle>/Contents/Resources on Mac OS X and is the installation
+ * directory on all other platforms. If false the file must be in the
+ * GRE Binary directory which is <bundle>/Contents/MacOS on Mac OS X
+ * and is the installation directory on on all other platforms.
+ */
+function copyFileToTestAppDir(aFileRelPath, aInGreDir) {
+ // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its
+ // properties
+ let srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone();
+ let destFile = aInGreDir ? getGREDir() : getGREBinDir();
+ let fileRelPath = aFileRelPath;
+ let pathParts = fileRelPath.split("/");
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ srcFile.append(pathParts[i]);
+ destFile.append(pathParts[i]);
+ }
+ }
+
+ if (IS_MACOSX && !srcFile.exists()) {
+ debugDump("unable to copy file since it doesn't exist! Checking if " +
+ fileRelPath + ".app exists. Path: " + srcFile.path);
+ // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its
+ // properties
+ srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone();
+ destFile = aInGreDir ? getGREDir() : getGREBinDir();
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ srcFile.append(pathParts[i] + (pathParts.length - 1 == i ? ".app" : ""));
+ destFile.append(pathParts[i] + (pathParts.length - 1 == i ? ".app" : ""));
+ }
+ }
+ fileRelPath = fileRelPath + ".app";
+ }
+ Assert.ok(srcFile.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + srcFile.leafName);
+
+ // Symlink libraries. Note that the XUL library on Mac OS X doesn't have a
+ // file extension and shouldSymlink will always be false on Windows.
+ let shouldSymlink = (pathParts[pathParts.length - 1] == "XUL" ||
+ fileRelPath.substr(fileRelPath.length - 3) == ".so" ||
+ fileRelPath.substr(fileRelPath.length - 6) == ".dylib");
+ if (!shouldSymlink) {
+ if (!destFile.exists()) {
+ try {
+ srcFile.copyToFollowingLinks(destFile.parent, destFile.leafName);
+ } catch (e) {
+ // Just in case it is partially copied
+ if (destFile.exists()) {
+ try {
+ destFile.remove(true);
+ } catch (ex) {
+ logTestInfo("unable to remove file that failed to copy! Path: " +
+ destFile.path);
+ }
+ }
+ do_throw("Unable to copy file! Path: " + srcFile.path +
+ ", Exception: " + ex);
+ }
+ }
+ } else {
+ try {
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+ let ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
+ ln.initWithPath("/bin/ln");
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(ln);
+ let args = ["-s", srcFile.path, destFile.path];
+ process.run(true, args, args.length);
+ Assert.ok(destFile.isSymlink(),
+ destFile.leafName + " should be a symlink");
+ } catch (e) {
+ do_throw("Unable to create symlink for file! Path: " + srcFile.path +
+ ", Exception: " + e);
+ }
+ }
+}
+
+/**
+ * Attempts to upgrade the maintenance service if permissions are allowed.
+ * This is useful for XP where we have permission to upgrade in case an
+ * older service installer exists. Also if the user manually installed into
+ * a unprivileged location.
+ *
+ * @return true if the installed service is from this build. If the installed
+ * service is not from this build the test will fail instead of
+ * returning false.
+ */
+function attemptServiceInstall() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let maintSvcDir = getMaintSvcDir();
+ Assert.ok(maintSvcDir.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + maintSvcDir.leafName);
+ let oldMaintSvcBin = maintSvcDir.clone();
+ oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ Assert.ok(oldMaintSvcBin.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + oldMaintSvcBin.leafName);
+ let buildMaintSvcBin = getGREBinDir();
+ buildMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ if (readFileBytes(oldMaintSvcBin) == readFileBytes(buildMaintSvcBin)) {
+ debugDump("installed maintenance service binary is the same as the " +
+ "build's maintenance service binary");
+ return true;
+ }
+ let backupMaintSvcBin = maintSvcDir.clone();
+ backupMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN + ".backup");
+ try {
+ if (backupMaintSvcBin.exists()) {
+ backupMaintSvcBin.remove(false);
+ }
+ oldMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN + ".backup");
+ buildMaintSvcBin.copyTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN);
+ backupMaintSvcBin.remove(false);
+ } catch (e) {
+ // Restore the original file in case the moveTo was successful.
+ if (backupMaintSvcBin.exists()) {
+ oldMaintSvcBin = maintSvcDir.clone();
+ oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ if (!oldMaintSvcBin.exists()) {
+ backupMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN);
+ }
+ }
+ Assert.ok(false, "should be able copy the test maintenance service to " +
+ "the maintenance service directory (if not, build system " +
+ "configuration bug?), path: " + maintSvcDir.path);
+ }
+
+ return true;
+}
+
+/**
+ * Waits for the applications that are launched by the maintenance service to
+ * stop.
+ */
+function waitServiceApps() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ // maintenanceservice_installer.exe is started async during updates.
+ waitForApplicationStop("maintenanceservice_installer.exe");
+ // maintenanceservice_tmp.exe is started async from the service installer.
+ waitForApplicationStop("maintenanceservice_tmp.exe");
+ // In case the SCM thinks the service is stopped, but process still exists.
+ waitForApplicationStop("maintenanceservice.exe");
+}
+
+/**
+ * Waits for the maintenance service to stop.
+ */
+function waitForServiceStop(aFailTest) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ waitServiceApps();
+ debugDump("waiting for the maintenance service to stop if necessary");
+ // Use the helper bin to ensure the service is stopped. If not stopped, then
+ // wait for the service to stop (at most 120 seconds).
+ let args = ["wait-for-service-stop", "MozillaMaintenance", "120"];
+ let exitValue = runTestHelperSync(args);
+ Assert.notEqual(exitValue, 0xEE,
+ "the maintenance service should exist");
+ if (exitValue != 0) {
+ if (aFailTest) {
+ Assert.ok(false, "the maintenance service should stop, process exit " +
+ "value: " + exitValue);
+ }
+ logTestInfo("maintenance service did not stop which may cause test " +
+ "failures later, process exit value: " + exitValue);
+ } else {
+ debugDump("service stopped");
+ }
+ waitServiceApps();
+}
+
+/**
+ * Waits for the specified application to stop.
+ *
+ * @param aApplication
+ * The application binary name to wait until it has stopped.
+ */
+function waitForApplicationStop(aApplication) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ debugDump("waiting for " + aApplication + " to stop if necessary");
+ // Use the helper bin to ensure the application is stopped. If not stopped,
+ // then wait for it to stop (at most 120 seconds).
+ let args = ["wait-for-application-exit", aApplication, "120"];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0,
+ "the process should have stopped, process name: " +
+ aApplication);
+}
+
+
+/**
+ * Gets the platform specific shell binary that is launched using nsIProcess and
+ * in turn launches a binary used for the test (e.g. application, updater,
+ * etc.). A shell is used so debug console output can be redirected to a file so
+ * it doesn't end up in the test log.
+ *
+ * @return nsIFile for the shell binary to launch using nsIProcess.
+ */
+function getLaunchBin() {
+ let launchBin;
+ if (IS_WIN) {
+ launchBin = Services.dirsvc.get("WinD", Ci.nsIFile);
+ launchBin.append("System32");
+ launchBin.append("cmd.exe");
+ } else {
+ launchBin = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ launchBin.initWithPath("/bin/sh");
+ }
+ Assert.ok(launchBin.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(launchBin.path));
+
+ return launchBin;
+}
+
+
+/**
+ * Locks a Windows directory.
+ *
+ * @param aDirPath
+ * The test file object that describes the file to make in use.
+ */
+function lockDirectory(aDirPath) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ debugDump("start - locking installation directory");
+ const LPCWSTR = ctypes.char16_t.ptr;
+ const DWORD = ctypes.uint32_t;
+ const LPVOID = ctypes.voidptr_t;
+ const GENERIC_READ = 0x80000000;
+ const FILE_SHARE_READ = 1;
+ const FILE_SHARE_WRITE = 2;
+ const OPEN_EXISTING = 3;
+ const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
+ const INVALID_HANDLE_VALUE = LPVOID(0xffffffff);
+ let kernel32 = ctypes.open("kernel32");
+ let CreateFile = kernel32.declare("CreateFileW", ctypes.default_abi,
+ LPVOID, LPCWSTR, DWORD, DWORD,
+ LPVOID, DWORD, DWORD, LPVOID);
+ gHandle = CreateFile(aDirPath, GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE, LPVOID(0),
+ OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, LPVOID(0));
+ Assert.notEqual(gHandle.toString(), INVALID_HANDLE_VALUE.toString(),
+ "the handle should not equal INVALID_HANDLE_VALUE");
+ kernel32.close();
+ debugDump("finish - locking installation directory");
+}
+
+/**
+ * Launches the test helper binary to make it in use for updater tests and then
+ * calls waitForHelperSleep.
+ *
+ * @param aTestFile
+ * The test file object that describes the file to make in use.
+ */
+function runHelperFileInUse(aRelPath, aCopyTestHelper) {
+ logTestInfo("aRelPath: " + aRelPath);
+ // Launch an existing file so it is in use during the update.
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let fileInUseBin = getApplyDirFile(aRelPath);
+ if (aCopyTestHelper) {
+ fileInUseBin.remove(false);
+ helperBin.copyTo(fileInUseBin.parent, fileInUseBin.leafName);
+ }
+ fileInUseBin.permissions = PERMS_DIRECTORY;
+ let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s",
+ HELPER_SLEEP_TIMEOUT];
+ let fileInUseProcess = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ fileInUseProcess.init(fileInUseBin);
+ fileInUseProcess.run(false, args, args.length);
+
+ do_execute_soon(waitForHelperSleep);
+}
+
+/**
+ * Launches the test helper binary and locks a file specified on the command
+ * line for updater tests and then calls waitForHelperSleep.
+ *
+ * @param aTestFile
+ * The test file object that describes the file to lock.
+ */
+function runHelperLockFile(aTestFile) {
+ // Exclusively lock an existing file so it is in use during the update.
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let helperDestDir = getApplyDirFile(DIR_RESOURCES);
+ helperBin.copyTo(helperDestDir, FILE_HELPER_BIN);
+ helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN);
+ // Strip off the first two directories so the path has to be from the helper's
+ // working directory.
+ let lockFileRelPath = aTestFile.relPathDir.split("/");
+ if (IS_MACOSX) {
+ lockFileRelPath = lockFileRelPath.slice(2);
+ }
+ lockFileRelPath = lockFileRelPath.join("/") + "/" + aTestFile.fileName;
+ let args = [getApplyDirPath() + DIR_RESOURCES, "input", "output", "-s",
+ HELPER_SLEEP_TIMEOUT, lockFileRelPath];
+ let helperProcess = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ helperProcess.init(helperBin);
+ helperProcess.run(false, args, args.length);
+
+ do_execute_soon(waitForHelperSleep);
+}
+
+/**
+ * Helper function that waits until the helper has completed its operations and
+ * calls waitForHelperSleepFinished when it is finished.
+ */
+function waitForHelperSleep() {
+ gTimeoutRuns++;
+ // Give the lock file process time to lock the file before updating otherwise
+ // this test can fail intermittently on Windows debug builds.
+ let output = getApplyDirFile(DIR_RESOURCES + "output", true);
+ if (readFile(output) != "sleeping\n") {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the helper to " +
+ "finish its operation. Path: " + output.path);
+ }
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperSleep);
+ return;
+ }
+ try {
+ output.remove(false);
+ } catch (e) {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the helper " +
+ "message file to no longer be in use. Path: " + output.path);
+ }
+ debugDump("failed to remove file. Path: " + output.path);
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperSleep);
+ return;
+ }
+ waitForHelperSleepFinished();
+}
+
+/**
+ * Helper function that waits until the helper has finished its operations
+ * before calling waitForHelperFinishFileUnlock to verify that the helper's
+ * input and output directories are no longer in use.
+ */
+function waitForHelperFinished() {
+ // Give the lock file process time to lock the file before updating otherwise
+ // this test can fail intermittently on Windows debug builds.
+ let output = getApplyDirFile(DIR_RESOURCES + "output", true);
+ if (readFile(output) != "finished\n") {
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForHelperFinished);
+ return;
+ }
+ // Give the lock file process time to unlock the file before deleting the
+ // input and output files.
+ waitForHelperFinishFileUnlock();
+}
+
+/**
+ * Helper function that waits until the helper's input and output files are no
+ * longer in use before calling waitForHelperExitFinished.
+ */
+function waitForHelperFinishFileUnlock() {
+ try {
+ let output = getApplyDirFile(DIR_RESOURCES + "output", true);
+ if (output.exists()) {
+ output.remove(false);
+ }
+ let input = getApplyDirFile(DIR_RESOURCES + "input", true);
+ if (input.exists()) {
+ input.remove(false);
+ }
+ } catch (e) {
+ // Give the lock file process time to unlock the file before deleting the
+ // input and output files.
+ do_execute_soon(waitForHelperFinishFileUnlock);
+ return;
+ }
+ do_execute_soon(waitForHelperExitFinished);
+}
+
+/**
+ * Helper function to tell the helper to finish and exit its sleep state.
+ */
+function waitForHelperExit() {
+ let input = getApplyDirFile(DIR_RESOURCES + "input", true);
+ writeFile(input, "finish\n");
+ waitForHelperFinished();
+}
+
+/**
+ * Helper function for updater binary tests that creates the files and
+ * directories used by the test.
+ *
+ * @param aMarFile
+ * The mar file for the update test.
+ * @param aPostUpdateAsync
+ * When null the updater.ini is not created otherwise this parameter
+ * is passed to createUpdaterINI.
+ * @param aPostUpdateExeRelPathPrefix
+ * When aPostUpdateAsync null this value is ignored otherwise it is
+ * passed to createUpdaterINI.
+ */
+function setupUpdaterTest(aMarFile, aPostUpdateAsync,
+ aPostUpdateExeRelPathPrefix = "") {
+ let updatesPatchDir = getUpdatesPatchDir();
+ if (!updatesPatchDir.exists()) {
+ updatesPatchDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+ // Copy the mar that will be applied
+ let mar = getTestDirFile(aMarFile);
+ mar.copyToFollowingLinks(updatesPatchDir, FILE_UPDATE_MAR);
+
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ helperBin.permissions = PERMS_DIRECTORY;
+ let afterApplyBinDir = getApplyDirFile(DIR_RESOURCES, true);
+ helperBin.copyToFollowingLinks(afterApplyBinDir, gCallbackBinFile);
+ helperBin.copyToFollowingLinks(afterApplyBinDir, gPostUpdateBinFile);
+
+ gTestFiles.forEach(function SUT_TF_FE(aTestFile) {
+ if (aTestFile.originalFile || aTestFile.originalContents) {
+ let testDir = getApplyDirFile(aTestFile.relPathDir, true);
+ if (!testDir.exists()) {
+ testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+
+ let testFile;
+ if (aTestFile.originalFile) {
+ testFile = getTestDirFile(aTestFile.originalFile);
+ testFile.copyToFollowingLinks(testDir, aTestFile.fileName);
+ testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName);
+ } else {
+ testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName,
+ true);
+ writeFile(testFile, aTestFile.originalContents);
+ }
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (!IS_WIN && aTestFile.originalPerms) {
+ testFile.permissions = aTestFile.originalPerms;
+ // Store the actual permissions on the file for reference later after
+ // setting the permissions.
+ if (!aTestFile.comparePerms) {
+ aTestFile.comparePerms = testFile.permissions;
+ }
+ }
+ }
+ });
+
+ // Add the test directory that will be updated for a successful update or left
+ // in the initial state for a failed update.
+ gTestDirs.forEach(function SUT_TD_FE(aTestDir) {
+ let testDir = getApplyDirFile(aTestDir.relPathDir, true);
+ if (!testDir.exists()) {
+ testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function SUT_TD_F_FE(aTestFile) {
+ let testFile = getApplyDirFile(aTestDir.relPathDir + aTestFile, true);
+ if (!testFile.exists()) {
+ testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function SUT_TD_SD_FE(aSubDir) {
+ let testSubDir = getApplyDirFile(aTestDir.relPathDir + aSubDir, true);
+ if (!testSubDir.exists()) {
+ testSubDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function SUT_TD_SDF_FE(aTestFile) {
+ let testFile = getApplyDirFile(aTestDir.relPathDir + aSubDir + aTestFile, true);
+ if (!testFile.exists()) {
+ testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ });
+ }
+ });
+ }
+ });
+
+ setupActiveUpdate();
+
+ if (aPostUpdateAsync !== null) {
+ createUpdaterINI(aPostUpdateAsync, aPostUpdateExeRelPathPrefix);
+ }
+
+ setupAppFilesAsync();
+}
+
+/**
+ * Helper function for updater binary tests that creates the update-settings.ini
+ * file.
+ */
+function createUpdateSettingsINI() {
+ let ini = getApplyDirFile(DIR_RESOURCES + FILE_UPDATE_SETTINGS_INI, true);
+ writeFile(ini, UPDATE_SETTINGS_CONTENTS);
+}
+
+/**
+ * Helper function for updater binary tests that creates the updater.ini
+ * file.
+ *
+ * @param aIsExeAsync
+ * True or undefined if the post update process should be async. If
+ * undefined ExeAsync will not be added to the updater.ini file in
+ * order to test the default launch behavior which is async.
+ * @param aExeRelPathPrefix
+ * A string to prefix the ExeRelPath values in the updater.ini.
+ */
+function createUpdaterINI(aIsExeAsync, aExeRelPathPrefix) {
+ let exeArg = "ExeArg=post-update-async\n";
+ let exeAsync = "";
+ if (aIsExeAsync !== undefined) {
+ if (aIsExeAsync) {
+ exeAsync = "ExeAsync=true\n";
+ } else {
+ exeArg = "ExeArg=post-update-sync\n";
+ exeAsync = "ExeAsync=false\n";
+ }
+ }
+
+ if (aExeRelPathPrefix && IS_WIN) {
+ aExeRelPathPrefix = aExeRelPathPrefix.replace("/", "\\");
+ }
+
+ let exeRelPathMac = "ExeRelPath=" + aExeRelPathPrefix + DIR_RESOURCES +
+ gPostUpdateBinFile + "\n";
+ let exeRelPathWin = "ExeRelPath=" + aExeRelPathPrefix + gPostUpdateBinFile + "\n";
+ let updaterIniContents = "[Strings]\n" +
+ "Title=Update Test\n" +
+ "Info=Running update test " + gTestID + "\n\n" +
+ "[PostUpdateMac]\n" +
+ exeRelPathMac +
+ exeArg +
+ exeAsync +
+ "\n" +
+ "[PostUpdateWin]\n" +
+ exeRelPathWin +
+ exeArg +
+ exeAsync;
+ let updaterIni = getApplyDirFile(DIR_RESOURCES + FILE_UPDATER_INI, true);
+ writeFile(updaterIni, updaterIniContents);
+}
+
+/**
+ * Gets the message log path used for assert checks to lessen the length printed
+ * to the log file.
+ *
+ * @param aPath
+ * The path to shorten for the log file.
+ * @return the message including the shortened path for the log file.
+ */
+function getMsgPath(aPath) {
+ return ", path: " + replaceLogPaths(aPath);
+}
+
+/**
+ * Helper function that replaces the common part of paths in the update log's
+ * contents with <test_dir_path> for paths to the the test directory and
+ * <update_dir_path> for paths to the update directory. This is needed since
+ * Assert.equal will truncate what it prints to the xpcshell log file.
+ *
+ * @param aLogContents
+ * The update log file's contents.
+ * @return the log contents with the paths replaced.
+ */
+function replaceLogPaths(aLogContents) {
+ let logContents = aLogContents;
+ // Remove the majority of the path up to the test directory. This is needed
+ // since Assert.equal won't print long strings to the test logs.
+ let testDirPath = do_get_file(gTestID, false).path;
+ if (IS_WIN) {
+ // Replace \\ with \\\\ so the regexp works.
+ testDirPath = testDirPath.replace(/\\/g, "\\\\");
+ }
+ logContents = logContents.replace(new RegExp(testDirPath, "g"),
+ "<test_dir_path>/" + gTestID);
+ let updatesDirPath = getMockUpdRootD().path;
+ if (IS_WIN) {
+ // Replace \\ with \\\\ so the regexp works.
+ updatesDirPath = updatesDirPath.replace(/\\/g, "\\\\");
+ }
+ logContents = logContents.replace(new RegExp(updatesDirPath, "g"),
+ "<update_dir_path>/" + gTestID);
+ if (IS_WIN) {
+ // Replace \ with /
+ logContents = logContents.replace(/\\/g, "/");
+ }
+ return logContents;
+}
+
+/**
+ * Helper function for updater binary tests for verifying the contents of the
+ * update log after a successful update.
+ *
+ * @param aCompareLogFile
+ * The log file to compare the update log with.
+ * @param aStaged
+ * If the update log file is for a staged update.
+ * @param aReplace
+ * If the update log file is for a replace update.
+ * @param aExcludeDistDir
+ * Removes lines containing the distribution directory from the log
+ * file to compare the update log with.
+ */
+function checkUpdateLogContents(aCompareLogFile, aStaged = false,
+ aReplace = false, aExcludeDistDir = false) {
+ if (IS_UNIX && !IS_MACOSX) {
+ // The order that files are returned when enumerating the file system on
+ // Linux is not deterministic so skip checking the logs.
+ return;
+ }
+
+ let updateLog = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ let updateLogContents = readFileBytes(updateLog);
+
+ // The channel-prefs.js is defined in gTestFilesCommon which will always be
+ // located to the end of gTestFiles when it is present.
+ if (gTestFiles.length > 1 &&
+ gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" &&
+ !gTestFiles[gTestFiles.length - 1].originalContents) {
+ updateLogContents = updateLogContents.replace(/.*defaults\/.*/g, "");
+ }
+
+ if (gTestFiles.length > 2 &&
+ gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI &&
+ !gTestFiles[gTestFiles.length - 2].originalContents) {
+ updateLogContents = updateLogContents.replace(/.*update-settings.ini.*/g, "");
+ }
+
+ // Skip the source/destination lines since they contain absolute paths.
+ // These could be changed to relative paths using <test_dir_path> and
+ // <update_dir_path>
+ updateLogContents = updateLogContents.replace(/PATCH DIRECTORY.*/g, "");
+ updateLogContents = updateLogContents.replace(/INSTALLATION DIRECTORY.*/g, "");
+ updateLogContents = updateLogContents.replace(/WORKING DIRECTORY.*/g, "");
+ // Skip lines that log failed attempts to open the callback executable.
+ updateLogContents = updateLogContents.replace(/NS_main: callback app file .*/g, "");
+
+ if (IS_MACOSX) {
+ // Skip lines that log moving the distribution directory for Mac v2 signing.
+ updateLogContents = updateLogContents.replace(/Moving old [^\n]*\nrename_file: .*/g, "");
+ updateLogContents = updateLogContents.replace(/New distribution directory .*/g, "");
+ }
+
+ if (IS_WIN) {
+ // The FindFile results when enumerating the filesystem on Windows is not
+ // determistic so the results matching the following need to be fixed.
+ let re = new RegExp("([^\n]* 7\/7text1[^\n]*)\n" +
+ "([^\n]* 7\/7text0[^\n]*)\n", "g");
+ updateLogContents = updateLogContents.replace(re, "$2\n$1\n");
+ }
+
+ if (aReplace) {
+ // Remove the lines which contain absolute paths
+ updateLogContents = updateLogContents.replace(/^Begin moving.*$/mg, "");
+ updateLogContents = updateLogContents.replace(/^ensure_remove: failed to remove file: .*$/mg, "");
+ updateLogContents = updateLogContents.replace(/^ensure_remove_recursive: unable to remove directory: .*$/mg, "");
+ updateLogContents = updateLogContents.replace(/^Removing tmpDir failed, err: -1$/mg, "");
+ updateLogContents = updateLogContents.replace(/^remove_recursive_on_reboot: .*$/mg, "");
+ }
+
+ // Remove carriage returns.
+ updateLogContents = updateLogContents.replace(/\r/g, "");
+ // Replace error codes since they are different on each platform.
+ updateLogContents = updateLogContents.replace(/, err:.*\n/g, "\n");
+ // Replace to make the log parsing happy.
+ updateLogContents = updateLogContents.replace(/non-fatal error /g, "");
+ // Remove consecutive newlines
+ updateLogContents = updateLogContents.replace(/\n+/g, "\n");
+ // Remove leading and trailing newlines
+ updateLogContents = updateLogContents.replace(/^\n|\n$/g, "");
+ // Replace the log paths with <test_dir_path> and <update_dir_path>
+ updateLogContents = replaceLogPaths(updateLogContents);
+
+ let compareLogContents = "";
+ if (aCompareLogFile) {
+ compareLogContents = readFileBytes(getTestDirFile(aCompareLogFile));
+ }
+
+ if (aStaged) {
+ compareLogContents = PERFORMING_STAGED_UPDATE + "\n" + compareLogContents;
+ }
+
+ // The channel-prefs.js is defined in gTestFilesCommon which will always be
+ // located to the end of gTestFiles.
+ if (gTestFiles.length > 1 &&
+ gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" &&
+ !gTestFiles[gTestFiles.length - 1].originalContents) {
+ compareLogContents = compareLogContents.replace(/.*defaults\/.*/g, "");
+ }
+
+ if (gTestFiles.length > 2 &&
+ gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI &&
+ !gTestFiles[gTestFiles.length - 2].originalContents) {
+ compareLogContents = compareLogContents.replace(/.*update-settings.ini.*/g, "");
+ }
+
+ if (aExcludeDistDir) {
+ compareLogContents = compareLogContents.replace(/.*distribution\/.*/g, "");
+ }
+
+ // Remove leading and trailing newlines
+ compareLogContents = compareLogContents.replace(/\n+/g, "\n");
+ // Remove leading and trailing newlines
+ compareLogContents = compareLogContents.replace(/^\n|\n$/g, "");
+
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (compareLogContents == updateLogContents) {
+ Assert.ok(true, "the update log contents" + MSG_SHOULD_EQUAL);
+ } else {
+ logTestInfo("the update log contents are not correct");
+ logUpdateLog(FILE_LAST_UPDATE_LOG);
+ let aryLog = updateLogContents.split("\n");
+ let aryCompare = compareLogContents.split("\n");
+ // Pushing an empty string to both arrays makes it so either array's length
+ // can be used in the for loop below without going out of bounds.
+ aryLog.push("");
+ aryCompare.push("");
+ // xpcshell tests won't display the entire contents so log the first
+ // incorrect line.
+ for (let i = 0; i < aryLog.length; ++i) {
+ if (aryLog[i] != aryCompare[i]) {
+ logTestInfo("the first incorrect line in the update log is: " +
+ aryLog[i]);
+ Assert.equal(aryLog[i], aryCompare[i],
+ "the update log contents" + MSG_SHOULD_EQUAL);
+ }
+ }
+ // This should never happen!
+ do_throw("Unable to find incorrect update log contents!");
+ }
+}
+
+/**
+ * Helper function to check if the update log contains a string.
+ *
+ * @param aCheckString
+ * The string to check if the update log contains.
+ */
+function checkUpdateLogContains(aCheckString) {
+ let updateLog = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
+ updateLogContents = replaceLogPaths(updateLogContents);
+ Assert.notEqual(updateLogContents.indexOf(aCheckString), -1,
+ "the update log contents should contain value: " +
+ aCheckString);
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of files and
+ * directories after a successful update.
+ *
+ * @param aGetFileFunc
+ * The function used to get the files in the directory to be checked.
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateSuccess(aGetFileFunc, aStageDirExists = false,
+ aToBeDeletedDirExists = false) {
+ debugDump("testing contents of files after a successful update");
+ gTestFiles.forEach(function CFAUS_TF_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestFile.relPathDir + aTestFile.fileName, true);
+ debugDump("testing file: " + testFile.path);
+ if (aTestFile.compareFile || aTestFile.compareContents) {
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (!IS_WIN && aTestFile.comparePerms) {
+ // Check if the permssions as set in the complete mar file are correct.
+ Assert.equal(testFile.permissions & 0xfff,
+ aTestFile.comparePerms & 0xfff,
+ "the file permissions" + MSG_SHOULD_EQUAL);
+ }
+
+ let fileContents1 = readFileBytes(testFile);
+ let fileContents2 = aTestFile.compareFile ?
+ readFileBytes(getTestDirFile(aTestFile.compareFile)) :
+ aTestFile.compareContents;
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (fileContents1 == fileContents2) {
+ Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL);
+ } else {
+ Assert.equal(fileContents1, fileContents2,
+ "the file contents" + MSG_SHOULD_EQUAL);
+ }
+ } else {
+ Assert.ok(!testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path));
+ }
+ });
+
+ debugDump("testing operations specified in removed-files were performed " +
+ "after a successful update");
+ gTestDirs.forEach(function CFAUS_TD_FE(aTestDir) {
+ let testDir = aGetFileFunc(aTestDir.relPathDir, true);
+ debugDump("testing directory: " + testDir.path);
+ if (aTestDir.dirRemoved) {
+ Assert.ok(!testDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testDir.path));
+ } else {
+ Assert.ok(testDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testDir.path));
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true);
+ if (aTestDir.filesRemoved) {
+ Assert.ok(!testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path));
+ } else {
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+ }
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) {
+ let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true);
+ Assert.ok(testSubDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testSubDir.path));
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir +
+ aSubDir + aTestFile, true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+ });
+ }
+ });
+ }
+ }
+ });
+
+ checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists,
+ aToBeDeletedDirExists);
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of files and
+ * directories after a failed update.
+ *
+ * @param aGetFileFunc
+ * The function used to get the files in the directory to be checked.
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateFailure(aGetFileFunc, aStageDirExists = false,
+ aToBeDeletedDirExists = false) {
+ debugDump("testing contents of files after a failed update");
+ gTestFiles.forEach(function CFAUF_TF_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestFile.relPathDir + aTestFile.fileName, true);
+ debugDump("testing file: " + testFile.path);
+ if (aTestFile.compareFile || aTestFile.compareContents) {
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (!IS_WIN && aTestFile.comparePerms) {
+ // Check the original permssions are retained on the file.
+ Assert.equal(testFile.permissions & 0xfff,
+ aTestFile.comparePerms & 0xfff,
+ "the file permissions" + MSG_SHOULD_EQUAL);
+ }
+
+ let fileContents1 = readFileBytes(testFile);
+ let fileContents2 = aTestFile.compareFile ?
+ readFileBytes(getTestDirFile(aTestFile.compareFile)) :
+ aTestFile.compareContents;
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (fileContents1 == fileContents2) {
+ Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL);
+ } else {
+ Assert.equal(fileContents1, fileContents2,
+ "the file contents" + MSG_SHOULD_EQUAL);
+ }
+ } else {
+ Assert.ok(!testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path));
+ }
+ });
+
+ debugDump("testing operations specified in removed-files were not " +
+ "performed after a failed update");
+ gTestDirs.forEach(function CFAUF_TD_FE(aTestDir) {
+ let testDir = aGetFileFunc(aTestDir.relPathDir, true);
+ Assert.ok(testDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testDir.path));
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) {
+ let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true);
+ Assert.ok(testSubDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testSubDir.path));
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir +
+ aSubDir + aTestFile, true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+ });
+ }
+ });
+ }
+ });
+
+ checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists,
+ aToBeDeletedDirExists);
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of common
+ * files and directories after a successful or failed update.
+ *
+ * @param aGetFileFunc
+ * the function used to get the files in the directory to be checked.
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateCommon(aGetFileFunc, aStageDirExists,
+ aToBeDeletedDirExists) {
+ debugDump("testing extra directories");
+ let stageDir = getStageDirFile(null, true);
+ if (aStageDirExists) {
+ Assert.ok(stageDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(stageDir.path));
+ } else {
+ Assert.ok(!stageDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path));
+ }
+
+ let toBeDeletedDirExists = IS_WIN ? aToBeDeletedDirExists : false;
+ let toBeDeletedDir = getApplyDirFile(DIR_TOBEDELETED, true);
+ if (toBeDeletedDirExists) {
+ Assert.ok(toBeDeletedDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(toBeDeletedDir.path));
+ } else {
+ Assert.ok(!toBeDeletedDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(toBeDeletedDir.path));
+ }
+
+ let updatingDir = getApplyDirFile("updating", true);
+ Assert.ok(!updatingDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path));
+
+ if (stageDir.exists()) {
+ updatingDir = stageDir.clone();
+ updatingDir.append("updating");
+ Assert.ok(!updatingDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path));
+ }
+
+ debugDump("testing backup files should not be left behind in the " +
+ "application directory");
+ let applyToDir = getApplyDirFile(null, true);
+ checkFilesInDirRecursive(applyToDir, checkForBackupFiles);
+
+ if (stageDir.exists()) {
+ debugDump("testing backup files should not be left behind in the " +
+ "staging directory");
+ applyToDir = getApplyDirFile(null, true);
+ checkFilesInDirRecursive(stageDir, checkForBackupFiles);
+ }
+}
+
+/**
+ * Helper function for updater binary tests for verifying the contents of the
+ * updater callback application log which should contain the arguments passed to
+ * the callback application.
+ */
+function checkCallbackLog() {
+ let appLaunchLog = getApplyDirFile(DIR_RESOURCES + gCallbackArgs[1], true);
+ if (!appLaunchLog.exists()) {
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog);
+ return;
+ }
+
+ let expectedLogContents = gCallbackArgs.join("\n") + "\n";
+ let logContents = readFile(appLaunchLog);
+ // It is possible for the log file contents check to occur before the log file
+ // contents are completely written so wait until the contents are the expected
+ // value. If the contents are never the expected value then the test will
+ // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or
+ // the test harness times out the test.
+ if (logContents != expectedLogContents) {
+ gTimeoutRuns++;
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ logTestInfo("callback log contents are not correct");
+ // This file doesn't contain full paths so there is no need to call
+ // replaceLogPaths.
+ let aryLog = logContents.split("\n");
+ let aryCompare = expectedLogContents.split("\n");
+ // Pushing an empty string to both arrays makes it so either array's length
+ // can be used in the for loop below without going out of bounds.
+ aryLog.push("");
+ aryCompare.push("");
+ // xpcshell tests won't display the entire contents so log the incorrect
+ // line.
+ for (let i = 0; i < aryLog.length; ++i) {
+ if (aryLog[i] != aryCompare[i]) {
+ logTestInfo("the first incorrect line in the callback log is: " +
+ aryLog[i]);
+ Assert.equal(aryLog[i], aryCompare[i],
+ "the callback log contents" + MSG_SHOULD_EQUAL);
+ }
+ }
+ // This should never happen!
+ do_throw("Unable to find incorrect callback log contents!");
+ }
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog);
+ return;
+ }
+ Assert.ok(true, "the callback log contents" + MSG_SHOULD_EQUAL);
+
+ waitForFilesInUse();
+}
+
+/**
+ * Helper function for updater binary tests for getting the log and running
+ * files created by the test helper binary file when called with the post-update
+ * command line argument.
+ *
+ * @param aSuffix
+ * The string to append to the post update test helper binary path.
+ */
+function getPostUpdateFile(aSuffix) {
+ return getApplyDirFile(DIR_RESOURCES + gPostUpdateBinFile + aSuffix, true);
+}
+
+/**
+ * Checks the contents of the updater post update binary log. When completed
+ * checkPostUpdateAppLogFinished will be called.
+ */
+function checkPostUpdateAppLog() {
+ // Only Mac OS X and Windows support post update.
+ if (IS_MACOSX || IS_WIN) {
+ gTimeoutRuns++;
+ let postUpdateLog = getPostUpdateFile(".log");
+ if (!postUpdateLog.exists()) {
+ debugDump("postUpdateLog does not exist. Path: " + postUpdateLog.path);
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the post update " +
+ "process to create the post update log. Path: " +
+ postUpdateLog.path);
+ }
+ do_execute_soon(checkPostUpdateAppLog);
+ return;
+ }
+
+ let logContents = readFile(postUpdateLog);
+ // It is possible for the log file contents check to occur before the log file
+ // contents are completely written so wait until the contents are the expected
+ // value. If the contents are never the expected value then the test will
+ // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or
+ // the test harness times out the test.
+ if (logContents != "post-update\n") {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the post update " +
+ "process to create the expected contents in the post update log. Path: " +
+ postUpdateLog.path);
+ }
+ do_execute_soon(checkPostUpdateAppLog);
+ return;
+ }
+ Assert.ok(true, "the post update log contents" + MSG_SHOULD_EQUAL);
+ }
+
+ do_execute_soon(checkPostUpdateAppLogFinished);
+}
+
+/**
+ * Helper function to check if a file is in use on Windows by making a copy of
+ * a file and attempting to delete the original file. If the deletion is
+ * successful the copy of the original file is renamed to the original file's
+ * name and if the deletion is not successful the copy of the original file is
+ * deleted.
+ *
+ * @param aFile
+ * An nsIFile for the file to be checked if it is in use.
+ * @return true if the file can't be deleted and false otherwise.
+ */
+function isFileInUse(aFile) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ if (!aFile.exists()) {
+ debugDump("file does not exist, path: " + aFile.path);
+ return false;
+ }
+
+ let fileBak = aFile.parent;
+ fileBak.append(aFile.leafName + ".bak");
+ try {
+ if (fileBak.exists()) {
+ fileBak.remove(false);
+ }
+ aFile.copyTo(aFile.parent, fileBak.leafName);
+ aFile.remove(false);
+ fileBak.moveTo(aFile.parent, aFile.leafName);
+ debugDump("file is not in use, path: " + aFile.path);
+ return false;
+ } catch (e) {
+ debugDump("file in use, path: " + aFile.path + ", exception: " + e);
+ try {
+ if (fileBak.exists()) {
+ fileBak.remove(false);
+ }
+ } catch (ex) {
+ logTestInfo("unable to remove backup file, path: " +
+ fileBak.path + ", exception: " + ex);
+ }
+ }
+ return true;
+}
+
+/**
+ * Waits until files that are in use that break tests are no longer in use and
+ * then calls doTestFinish to end the test.
+ */
+function waitForFilesInUse() {
+ if (IS_WIN) {
+ let fileNames = [FILE_APP_BIN, FILE_UPDATER_BIN,
+ FILE_MAINTENANCE_SERVICE_INSTALLER_BIN];
+ for (let i = 0; i < fileNames.length; ++i) {
+ let file = getApplyDirFile(fileNames[i], true);
+ if (isFileInUse(file)) {
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForFilesInUse);
+ return;
+ }
+ }
+ }
+
+ debugDump("calling doTestFinish");
+ doTestFinish();
+}
+
+/**
+ * Helper function for updater binary tests for verifying there are no update
+ * backup files left behind after an update.
+ *
+ * @param aFile
+ * An nsIFile to check if it has moz-backup for its extension.
+ */
+function checkForBackupFiles(aFile) {
+ Assert.notEqual(getFileExtension(aFile), "moz-backup",
+ "the file's extension should not equal moz-backup" +
+ getMsgPath(aFile.path));
+}
+
+/**
+ * Helper function for updater binary tests for recursively enumerating a
+ * directory and calling a callback function with the file as a parameter for
+ * each file found.
+ *
+ * @param aDir
+ * A nsIFile for the directory to be deleted
+ * @param aCallback
+ * A callback function that will be called with the file as a
+ * parameter for each file found.
+ */
+function checkFilesInDirRecursive(aDir, aCallback) {
+ if (!aDir.exists()) {
+ do_throw("Directory must exist!");
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
+
+ if (entry.exists()) {
+ if (entry.isDirectory()) {
+ checkFilesInDirRecursive(entry, aCallback);
+ } else {
+ aCallback(entry);
+ }
+ }
+ }
+}
+
+
+/**
+ * Helper function to override the update prompt component to verify whether it
+ * is called or not.
+ *
+ * @param aCallback
+ * The callback to call if the update prompt component is called.
+ */
+function overrideUpdatePrompt(aCallback) {
+ Cu.import("resource://testing-common/MockRegistrar.jsm");
+ MockRegistrar.register("@mozilla.org/updates/update-prompt;1", UpdatePrompt, [aCallback]);
+}
+
+function UpdatePrompt(aCallback) {
+ this._callback = aCallback;
+
+ let fns = ["checkForUpdates", "showUpdateAvailable", "showUpdateDownloaded",
+ "showUpdateError", "showUpdateHistory", "showUpdateInstalled"];
+
+ fns.forEach(function UP_fns(aPromptFn) {
+ UpdatePrompt.prototype[aPromptFn] = function() {
+ if (!this._callback) {
+ return;
+ }
+
+ let callback = this._callback[aPromptFn];
+ if (!callback) {
+ return;
+ }
+
+ callback.apply(this._callback,
+ Array.prototype.slice.call(arguments));
+ };
+ });
+}
+
+UpdatePrompt.prototype = {
+ flags: Ci.nsIClassInfo.SINGLETON,
+ getScriptableHelper: () => null,
+ getInterfaces: function(aCount) {
+ let interfaces = [Ci.nsISupports, Ci.nsIUpdatePrompt];
+ aCount.value = interfaces.length;
+ return interfaces;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIClassInfo, Ci.nsIUpdatePrompt])
+};
+
+/* Update check listener */
+const updateCheckListener = {
+ onProgress: function UCL_onProgress(aRequest, aPosition, aTotalSize) {
+ },
+
+ onCheckComplete: function UCL_onCheckComplete(aRequest, aUpdates, aUpdateCount) {
+ gRequestURL = aRequest.channel.originalURI.spec;
+ gUpdateCount = aUpdateCount;
+ gUpdates = aUpdates;
+ debugDump("url = " + gRequestURL + ", " +
+ "request.status = " + aRequest.status + ", " +
+ "updateCount = " + aUpdateCount);
+ // Use a timeout to allow the XHR to complete
+ do_execute_soon(gCheckFunc);
+ },
+
+ onError: function UCL_onError(aRequest, aUpdate) {
+ gRequestURL = aRequest.channel.originalURI.spec;
+ gStatusCode = aRequest.status;
+ if (gStatusCode == 0) {
+ gStatusCode = aRequest.channel.QueryInterface(Ci.nsIRequest).status;
+ }
+ gStatusText = aUpdate.statusText ? aUpdate.statusText : null;
+ debugDump("url = " + gRequestURL + ", " +
+ "request.status = " + gStatusCode + ", " +
+ "update.statusText = " + gStatusText);
+ // Use a timeout to allow the XHR to complete
+ do_execute_soon(gCheckFunc.bind(null, aRequest, aUpdate));
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIUpdateCheckListener])
+};
+
+/* Update download listener - nsIRequestObserver */
+const downloadListener = {
+ onStartRequest: function DL_onStartRequest(aRequest, aContext) {
+ },
+
+ onProgress: function DL_onProgress(aRequest, aContext, aProgress, aMaxProgress) {
+ },
+
+ onStatus: function DL_onStatus(aRequest, aContext, aStatus, aStatusText) {
+ },
+
+ onStopRequest: function DL_onStopRequest(aRequest, aContext, aStatus) {
+ gStatusResult = aStatus;
+ // Use a timeout to allow the request to complete
+ do_execute_soon(gCheckFunc);
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIRequestObserver,
+ Ci.nsIProgressEventSink])
+};
+
+/**
+ * Helper for starting the http server used by the tests
+ */
+function start_httpserver() {
+ let dir = getTestDirFile();
+ debugDump("http server directory path: " + dir.path);
+
+ if (!dir.isDirectory()) {
+ do_throw("A file instead of a directory was specified for HttpServer " +
+ "registerDirectory! Path: " + dir.path);
+ }
+
+ let { HttpServer } = Cu.import("resource://testing-common/httpd.js", {});
+ gTestserver = new HttpServer();
+ gTestserver.registerDirectory("/", dir);
+ gTestserver.registerPathHandler("/" + gHTTPHandlerPath, pathHandler);
+ gTestserver.start(-1);
+ let testserverPort = gTestserver.identity.primaryPort;
+ gURLData = URL_HOST + ":" + testserverPort + "/";
+ debugDump("http server port = " + testserverPort);
+}
+
+/**
+ * Custom path handler for the http server
+ *
+ * @param aMetadata
+ * The http metadata for the request.
+ * @param aResponse
+ * The http response for the request.
+ */
+function pathHandler(aMetadata, aResponse) {
+ aResponse.setHeader("Content-Type", "text/xml", false);
+ aResponse.setStatusLine(aMetadata.httpVersion, gResponseStatusCode, "OK");
+ aResponse.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+}
+
+/**
+ * Helper for stopping the http server used by the tests
+ *
+ * @param aCallback
+ * The callback to call after stopping the http server.
+ */
+function stop_httpserver(aCallback) {
+ Assert.ok(!!aCallback, "the aCallback parameter should be defined");
+ gTestserver.stop(aCallback);
+}
+
+/**
+ * Creates an nsIXULAppInfo
+ *
+ * @param aID
+ * The ID of the test application
+ * @param aName
+ * A name for the test application
+ * @param aVersion
+ * The version of the application
+ * @param aPlatformVersion
+ * The gecko version of the application
+ */
+function createAppInfo(aID, aName, aVersion, aPlatformVersion) {
+ const XULAPPINFO_CONTRACTID = "@mozilla.org/xre/app-info;1";
+ const XULAPPINFO_CID = Components.ID("{c763b610-9d49-455a-bbd2-ede71682a1ac}");
+ let ifaces = [Ci.nsIXULAppInfo, Ci.nsIXULRuntime];
+ if (IS_WIN) {
+ ifaces.push(Ci.nsIWinAppHelper);
+ }
+ const XULAppInfo = {
+ vendor: APP_INFO_VENDOR,
+ name: aName,
+ ID: aID,
+ version: aVersion,
+ appBuildID: "2007010101",
+ platformVersion: aPlatformVersion,
+ platformBuildID: "2007010101",
+ inSafeMode: false,
+ logConsoleErrors: true,
+ OS: "XPCShell",
+ XPCOMABI: "noarch-spidermonkey",
+
+ QueryInterface: XPCOMUtils.generateQI(ifaces)
+ };
+
+ const XULAppInfoFactory = {
+ createInstance: function(aOuter, aIID) {
+ if (aOuter == null) {
+ return XULAppInfo.QueryInterface(aIID);
+ }
+ throw Cr.NS_ERROR_NO_AGGREGATION;
+ }
+ };
+
+ let registrar = Cm.QueryInterface(Ci.nsIComponentRegistrar);
+ registrar.registerFactory(XULAPPINFO_CID, "XULAppInfo",
+ XULAPPINFO_CONTRACTID, XULAppInfoFactory);
+}
+
+/**
+ * Returns the platform specific arguments used by nsIProcess when launching
+ * the application.
+ *
+ * @param aExtraArgs (optional)
+ * An array of extra arguments to append to the default arguments.
+ * @return an array of arguments to be passed to nsIProcess.
+ *
+ * Note: a shell is necessary to pipe the application's console output which
+ * would otherwise pollute the xpcshell log.
+ *
+ * Command line arguments used when launching the application:
+ * -no-remote prevents shell integration from being affected by an existing
+ * application process.
+ * -test-process-updates makes the application exit after being relaunched by
+ * the updater.
+ * the platform specific string defined by PIPE_TO_NULL to output both stdout
+ * and stderr to null. This is needed to prevent output from the application
+ * from ending up in the xpchsell log.
+ */
+function getProcessArgs(aExtraArgs) {
+ if (!aExtraArgs) {
+ aExtraArgs = [];
+ }
+
+ let appBinPath = getApplyDirFile(DIR_MACOS + FILE_APP_BIN, false).path;
+ if (/ /.test(appBinPath)) {
+ appBinPath = '"' + appBinPath + '"';
+ }
+
+ let args;
+ if (IS_UNIX) {
+ let launchScript = getLaunchScript();
+ // Precreate the script with executable permissions
+ launchScript.create(Ci.nsILocalFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY);
+
+ let scriptContents = "#! /bin/sh\n";
+ scriptContents += appBinPath + " -no-remote -test-process-updates " +
+ aExtraArgs.join(" ") + " " + PIPE_TO_NULL;
+ writeFile(launchScript, scriptContents);
+ debugDump("created " + launchScript.path + " containing:\n" +
+ scriptContents);
+ args = [launchScript.path];
+ } else {
+ args = ["/D", "/Q", "/C", appBinPath, "-no-remote", "-test-process-updates"].
+ concat(aExtraArgs).concat([PIPE_TO_NULL]);
+ }
+ return args;
+}
+
+/**
+ * Gets a file path for the application to dump its arguments into. This is used
+ * to verify that a callback application is launched.
+ *
+ * @return the file for the application to dump its arguments into.
+ */
+function getAppArgsLogPath() {
+ let appArgsLog = do_get_file("/" + gTestID + "_app_args_log", true);
+ if (appArgsLog.exists()) {
+ appArgsLog.remove(false);
+ }
+ let appArgsLogPath = appArgsLog.path;
+ if (/ /.test(appArgsLogPath)) {
+ appArgsLogPath = '"' + appArgsLogPath + '"';
+ }
+ return appArgsLogPath;
+}
+
+/**
+ * Gets the nsIFile reference for the shell script to launch the application. If
+ * the file exists it will be removed by this function.
+ *
+ * @return the nsIFile for the shell script to launch the application.
+ */
+function getLaunchScript() {
+ let launchScript = do_get_file("/" + gTestID + "_launch.sh", true);
+ if (launchScript.exists()) {
+ launchScript.remove(false);
+ }
+ return launchScript;
+}
+
+/**
+ * Makes GreD, XREExeF, and UpdRootD point to unique file system locations so
+ * xpcshell tests can run in parallel and to keep the environment clean.
+ */
+function adjustGeneralPaths() {
+ let dirProvider = {
+ getFile: function AGP_DP_getFile(aProp, aPersistent) {
+ aPersistent.value = true;
+ switch (aProp) {
+ case NS_GRE_DIR:
+ if (gUseTestAppDir) {
+ return getApplyDirFile(DIR_RESOURCES, true);
+ }
+ break;
+ case NS_GRE_BIN_DIR:
+ if (gUseTestAppDir) {
+ return getApplyDirFile(DIR_MACOS, true);
+ }
+ break;
+ case XRE_EXECUTABLE_FILE:
+ if (gUseTestAppDir) {
+ return getApplyDirFile(DIR_MACOS + FILE_APP_BIN, true);
+ }
+ break;
+ case XRE_UPDATE_ROOT_DIR:
+ return getMockUpdRootD();
+ }
+ return null;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider])
+ };
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_DIR);
+ ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_BIN_DIR);
+ ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE);
+ ds.registerProvider(dirProvider);
+ do_register_cleanup(function AGP_cleanup() {
+ debugDump("start - unregistering directory provider");
+
+ if (gAppTimer) {
+ debugDump("start - cancel app timer");
+ gAppTimer.cancel();
+ gAppTimer = null;
+ debugDump("finish - cancel app timer");
+ }
+
+ if (gProcess && gProcess.isRunning) {
+ debugDump("start - kill process");
+ try {
+ gProcess.kill();
+ } catch (e) {
+ debugDump("kill process failed. Exception: " + e);
+ }
+ gProcess = null;
+ debugDump("finish - kill process");
+ }
+
+ if (gHandle) {
+ try {
+ debugDump("start - closing handle");
+ let kernel32 = ctypes.open("kernel32");
+ let CloseHandle = kernel32.declare("CloseHandle", ctypes.default_abi,
+ ctypes.bool, /* return*/
+ ctypes.voidptr_t /* handle*/);
+ if (!CloseHandle(gHandle)) {
+ debugDump("call to CloseHandle failed");
+ }
+ kernel32.close();
+ gHandle = null;
+ debugDump("finish - closing handle");
+ } catch (e) {
+ debugDump("call to CloseHandle failed. Exception: " + e);
+ }
+ }
+
+ // Call end_test first before the directory provider is unregistered
+ if (typeof end_test == typeof Function) {
+ debugDump("calling end_test");
+ end_test();
+ }
+
+ ds.unregisterProvider(dirProvider);
+ cleanupTestCommon();
+
+ debugDump("finish - unregistering directory provider");
+ });
+}
+
+/**
+ * The timer callback to kill the process if it takes too long.
+ */
+const gAppTimerCallback = {
+ notify: function TC_notify(aTimer) {
+ gAppTimer = null;
+ if (gProcess.isRunning) {
+ logTestInfo("attempting to kill process");
+ gProcess.kill();
+ }
+ Assert.ok(false, "launch application timer expired");
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsITimerCallback])
+};
+
+/**
+ * Launches an application to apply an update.
+ */
+function runUpdateUsingApp(aExpectedStatus) {
+ /**
+ * The observer for the call to nsIProcess:runAsync. When completed
+ * runUpdateFinished will be called.
+ */
+ const processObserver = {
+ observe: function PO_observe(aSubject, aTopic, aData) {
+ debugDump("topic: " + aTopic + ", process exitValue: " +
+ gProcess.exitValue);
+ resetEnvironment();
+ if (gAppTimer) {
+ gAppTimer.cancel();
+ gAppTimer = null;
+ }
+ Assert.equal(gProcess.exitValue, 0,
+ "the application process exit value should be 0");
+ Assert.equal(aTopic, "process-finished",
+ "the application process observer topic should be " +
+ "process-finished");
+
+ if (IS_SERVICE_TEST) {
+ waitForServiceStop(false);
+ }
+
+ do_execute_soon(afterAppExits);
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver])
+ };
+
+ function afterAppExits() {
+ gTimeoutRuns++;
+
+ if (IS_WIN) {
+ waitForApplicationStop(FILE_UPDATER_BIN);
+ }
+
+ let status;
+ try {
+ status = readStatusFile();
+ } catch (e) {
+ logTestInfo("error reading status file, exception: " + e);
+ }
+ // Don't proceed until the update's status is the expected value.
+ if (status != aExpectedStatus) {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ logUpdateLog(FILE_UPDATE_LOG);
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " +
+ "status to equal: " +
+ aExpectedStatus +
+ ", current status: " + status);
+ } else {
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, afterAppExits);
+ }
+ return;
+ }
+
+ // Don't check for an update log when the code in nsUpdateDriver.cpp skips
+ // updating.
+ if (aExpectedStatus != STATE_PENDING &&
+ aExpectedStatus != STATE_PENDING_SVC &&
+ aExpectedStatus != STATE_APPLIED &&
+ aExpectedStatus != STATE_APPLIED_SVC) {
+ // Don't proceed until the update log has been created.
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ if (!log.exists()) {
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ do_throw("Exceeded MAX_TIMEOUT_RUNS while waiting for the update " +
+ "log to be created. Path: " + log.path);
+ }
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, afterAppExits);
+ return;
+ }
+ }
+
+ do_execute_soon(runUpdateFinished);
+ }
+
+ debugDump("start - launching application to apply update");
+
+ let appBin = getApplyDirFile(DIR_MACOS + FILE_APP_BIN, false);
+
+ let launchBin = getLaunchBin();
+ let args = getProcessArgs();
+ debugDump("launching " + launchBin.path + " " + args.join(" "));
+
+ gProcess = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ gProcess.init(launchBin);
+
+ gAppTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gAppTimer.initWithCallback(gAppTimerCallback, APP_TIMER_TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT);
+
+ setEnvironment();
+ debugDump("launching application");
+ gProcess.runAsync(args, args.length, processObserver);
+
+ debugDump("finish - launching application to apply update");
+}
+
+/**
+ * Sets the environment that will be used by the application process when it is
+ * launched.
+ */
+function setEnvironment() {
+ // Prevent setting the environment more than once.
+ if (gShouldResetEnv !== undefined) {
+ return;
+ }
+
+ gShouldResetEnv = true;
+
+ // See bug 1279108.
+ if (gEnv.exists("ASAN_OPTIONS")) {
+ gASanOptions = gEnv.get("ASAN_OPTIONS");
+ gEnv.set("ASAN_OPTIONS", gASanOptions + ":detect_leaks=0");
+ } else {
+ gEnv.set("ASAN_OPTIONS", "detect_leaks=0");
+ }
+
+ if (IS_WIN && !gEnv.exists("XRE_NO_WINDOWS_CRASH_DIALOG")) {
+ gAddedEnvXRENoWindowsCrashDialog = true;
+ debugDump("setting the XRE_NO_WINDOWS_CRASH_DIALOG environment " +
+ "variable to 1... previously it didn't exist");
+ gEnv.set("XRE_NO_WINDOWS_CRASH_DIALOG", "1");
+ }
+
+ if (IS_UNIX) {
+ let appGreBinDir = gGREBinDirOrig.clone();
+ let envGreBinDir = Cc["@mozilla.org/file/local;1"].
+ createInstance(Ci.nsILocalFile);
+ let shouldSetEnv = true;
+ if (IS_MACOSX) {
+ if (gEnv.exists("DYLD_LIBRARY_PATH")) {
+ gEnvDyldLibraryPath = gEnv.get("DYLD_LIBRARY_PATH");
+ envGreBinDir.initWithPath(gEnvDyldLibraryPath);
+ if (envGreBinDir.path == appGreBinDir.path) {
+ gEnvDyldLibraryPath = null;
+ shouldSetEnv = false;
+ }
+ }
+
+ if (shouldSetEnv) {
+ debugDump("setting DYLD_LIBRARY_PATH environment variable value to " +
+ appGreBinDir.path);
+ gEnv.set("DYLD_LIBRARY_PATH", appGreBinDir.path);
+ }
+ } else {
+ if (gEnv.exists("LD_LIBRARY_PATH")) {
+ gEnvLdLibraryPath = gEnv.get("LD_LIBRARY_PATH");
+ envGreBinDir.initWithPath(gEnvLdLibraryPath);
+ if (envGreBinDir.path == appGreBinDir.path) {
+ gEnvLdLibraryPath = null;
+ shouldSetEnv = false;
+ }
+ }
+
+ if (shouldSetEnv) {
+ debugDump("setting LD_LIBRARY_PATH environment variable value to " +
+ appGreBinDir.path);
+ gEnv.set("LD_LIBRARY_PATH", appGreBinDir.path);
+ }
+ }
+ }
+
+ if (gEnv.exists("XPCOM_MEM_LEAK_LOG")) {
+ gEnvXPCOMMemLeakLog = gEnv.get("XPCOM_MEM_LEAK_LOG");
+ debugDump("removing the XPCOM_MEM_LEAK_LOG environment variable... " +
+ "previous value " + gEnvXPCOMMemLeakLog);
+ gEnv.set("XPCOM_MEM_LEAK_LOG", "");
+ }
+
+ if (gEnv.exists("XPCOM_DEBUG_BREAK")) {
+ gEnvXPCOMDebugBreak = gEnv.get("XPCOM_DEBUG_BREAK");
+ debugDump("setting the XPCOM_DEBUG_BREAK environment variable to " +
+ "warn... previous value " + gEnvXPCOMDebugBreak);
+ } else {
+ debugDump("setting the XPCOM_DEBUG_BREAK environment variable to " +
+ "warn... previously it didn't exist");
+ }
+
+ gEnv.set("XPCOM_DEBUG_BREAK", "warn");
+
+ if (IS_SERVICE_TEST) {
+ debugDump("setting MOZ_NO_SERVICE_FALLBACK environment variable to 1");
+ gEnv.set("MOZ_NO_SERVICE_FALLBACK", "1");
+ }
+}
+
+/**
+ * Sets the environment back to the original values after launching the
+ * application.
+ */
+function resetEnvironment() {
+ // Prevent resetting the environment more than once.
+ if (gShouldResetEnv !== true) {
+ return;
+ }
+
+ gShouldResetEnv = false;
+
+ // Restore previous ASAN_OPTIONS if there were any.
+ gEnv.set("ASAN_OPTIONS", gASanOptions ? gASanOptions : "");
+
+ if (gEnvXPCOMMemLeakLog) {
+ debugDump("setting the XPCOM_MEM_LEAK_LOG environment variable back to " +
+ gEnvXPCOMMemLeakLog);
+ gEnv.set("XPCOM_MEM_LEAK_LOG", gEnvXPCOMMemLeakLog);
+ }
+
+ if (gEnvXPCOMDebugBreak) {
+ debugDump("setting the XPCOM_DEBUG_BREAK environment variable back to " +
+ gEnvXPCOMDebugBreak);
+ gEnv.set("XPCOM_DEBUG_BREAK", gEnvXPCOMDebugBreak);
+ } else if (gEnv.exists("XPCOM_DEBUG_BREAK")) {
+ debugDump("clearing the XPCOM_DEBUG_BREAK environment variable");
+ gEnv.set("XPCOM_DEBUG_BREAK", "");
+ }
+
+ if (IS_UNIX) {
+ if (IS_MACOSX) {
+ if (gEnvDyldLibraryPath) {
+ debugDump("setting DYLD_LIBRARY_PATH environment variable value " +
+ "back to " + gEnvDyldLibraryPath);
+ gEnv.set("DYLD_LIBRARY_PATH", gEnvDyldLibraryPath);
+ } else if (gEnvDyldLibraryPath !== null) {
+ debugDump("removing DYLD_LIBRARY_PATH environment variable");
+ gEnv.set("DYLD_LIBRARY_PATH", "");
+ }
+ } else if (gEnvLdLibraryPath) {
+ debugDump("setting LD_LIBRARY_PATH environment variable value back " +
+ "to " + gEnvLdLibraryPath);
+ gEnv.set("LD_LIBRARY_PATH", gEnvLdLibraryPath);
+ } else if (gEnvLdLibraryPath !== null) {
+ debugDump("removing LD_LIBRARY_PATH environment variable");
+ gEnv.set("LD_LIBRARY_PATH", "");
+ }
+ }
+
+ if (IS_WIN && gAddedEnvXRENoWindowsCrashDialog) {
+ debugDump("removing the XRE_NO_WINDOWS_CRASH_DIALOG environment " +
+ "variable");
+ gEnv.set("XRE_NO_WINDOWS_CRASH_DIALOG", "");
+ }
+
+ if (IS_SERVICE_TEST) {
+ debugDump("removing MOZ_NO_SERVICE_FALLBACK environment variable");
+ gEnv.set("MOZ_NO_SERVICE_FALLBACK", "");
+ }
+}
diff --git a/toolkit/mozapps/update/tests/moz.build b/toolkit/mozapps/update/tests/moz.build
new file mode 100644
index 000000000..842ec7f90
--- /dev/null
+++ b/toolkit/mozapps/update/tests/moz.build
@@ -0,0 +1,101 @@
+# -*- 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/.
+
+HAS_MISC_RULE = True
+
+FINAL_TARGET = '_tests/xpcshell/toolkit/mozapps/update/tests/data'
+
+MOCHITEST_CHROME_MANIFESTS += ['chrome/chrome.ini']
+
+XPCSHELL_TESTS_MANIFESTS += [
+ 'unit_aus_update/xpcshell.ini',
+ 'unit_base_updater/xpcshell.ini'
+]
+
+if CONFIG['MOZ_MAINTENANCE_SERVICE']:
+ XPCSHELL_TESTS_MANIFESTS += ['unit_service_updater/xpcshell.ini']
+
+SimplePrograms([
+ 'TestAUSHelper',
+ 'TestAUSReadStrings',
+])
+
+LOCAL_INCLUDES += [
+ '/toolkit/mozapps/update',
+ '/toolkit/mozapps/update/common',
+]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ USE_LIBS += [
+ 'updatecommon-standalone',
+ ]
+
+ OS_LIBS += [
+ 'shlwapi',
+ ]
+else:
+ USE_LIBS += [
+ 'updatecommon',
+ ]
+
+for var in ('MOZ_APP_NAME', 'MOZ_APP_BASENAME', 'MOZ_APP_DISPLAYNAME',
+ 'MOZ_APP_VENDOR', 'BIN_SUFFIX', 'MOZ_DEBUG'):
+ DEFINES[var] = CONFIG[var]
+
+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
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ DEFINES['UNICODE'] = True
+ DEFINES['_UNICODE'] = True
+ USE_STATIC_LIBS = True
+ if CONFIG['GNU_CC']:
+ WIN32_EXE_LDFLAGS += ['-municode']
+
+TEST_HARNESS_FILES.testing.mochitest.chrome.toolkit.mozapps.update.tests.data += [
+ 'data/shared.js',
+ 'data/sharedUpdateXML.js',
+ 'data/simple.mar',
+]
+
+FINAL_TARGET_FILES += [
+ 'data/complete.exe',
+ 'data/complete.mar',
+ 'data/complete.png',
+ 'data/complete_log_success_mac',
+ 'data/complete_log_success_win',
+ 'data/complete_mac.mar',
+ 'data/complete_precomplete',
+ 'data/complete_precomplete_mac',
+ 'data/complete_removed-files',
+ 'data/complete_removed-files_mac',
+ 'data/complete_update_manifest',
+ 'data/old_version.mar',
+ 'data/partial.exe',
+ 'data/partial.mar',
+ 'data/partial.png',
+ 'data/partial_log_failure_mac',
+ 'data/partial_log_failure_win',
+ 'data/partial_log_success_mac',
+ 'data/partial_log_success_win',
+ 'data/partial_mac.mar',
+ 'data/partial_precomplete',
+ 'data/partial_precomplete_mac',
+ 'data/partial_removed-files',
+ 'data/partial_removed-files_mac',
+ 'data/partial_update_manifest',
+ 'data/replace_log_success',
+ 'data/shared.js',
+ 'data/sharedUpdateXML.js',
+ 'data/simple.mar',
+ 'data/wrong_product_channel.mar',
+ 'data/xpcshellUtilsAUS.js',
+]
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js b/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js
new file mode 100644
index 000000000..1985df959
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/canCheckForAndCanApplyUpdates.js
@@ -0,0 +1,138 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ // Verify write access to the custom app dir
+ debugDump("testing write access to the application directory");
+ let testFile = getCurrentProcessDir();
+ testFile.append("update_write_access_test");
+ testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ Assert.ok(testFile.exists(), MSG_SHOULD_EXIST);
+ testFile.remove(false);
+ Assert.ok(!testFile.exists(), MSG_SHOULD_NOT_EXIST);
+
+ standardInit();
+
+ if (IS_WIN) {
+ // Create a mutex to prevent being able to check for or apply updates.
+ debugDump("attempting to create mutex");
+ let handle = createMutex(getPerInstallationMutexName());
+ Assert.ok(!!handle, "the update mutex should have been created");
+
+ // Check if available updates cannot be checked for when there is a mutex
+ // for this installation.
+ Assert.ok(!gAUS.canCheckForUpdates, "should not be able to check for " +
+ "updates when there is an update mutex");
+
+ // Check if updates cannot be applied when there is a mutex for this
+ // installation.
+ Assert.ok(!gAUS.canApplyUpdates, "should not be able to apply updates " +
+ "when there is an update mutex");
+
+ debugDump("destroying mutex");
+ closeHandle(handle);
+ }
+
+ // Check if available updates can be checked for
+ Assert.ok(gAUS.canCheckForUpdates, "should be able to check for updates");
+ // Check if updates can be applied
+ Assert.ok(gAUS.canApplyUpdates, "should be able to apply updates");
+
+ if (IS_WIN) {
+ // Attempt to create a mutex when application update has already created one
+ // with the same name.
+ debugDump("attempting to create mutex");
+ let handle = createMutex(getPerInstallationMutexName());
+
+ Assert.ok(!handle, "should not be able to create the update mutex when " +
+ "the application has created the update mutex");
+ }
+
+ doTestFinish();
+}
+
+/**
+ * Determines a unique mutex name for the installation.
+ *
+ * @return Global mutex path.
+ */
+function getPerInstallationMutexName() {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].
+ createInstance(Ci.nsICryptoHash);
+ hasher.init(hasher.SHA1);
+
+ let exeFile = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsILocalFile);
+
+ let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"].
+ createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let data = converter.convertToByteArray(exeFile.path.toLowerCase());
+
+ hasher.update(data, data.length);
+ return "Global\\MozillaUpdateMutex-" + hasher.finish(true);
+}
+
+/**
+ * Closes a Win32 handle.
+ *
+ * @param aHandle
+ * The handle to close.
+ */
+function closeHandle(aHandle) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let lib = ctypes.open("kernel32.dll");
+ let CloseHandle = lib.declare("CloseHandle",
+ ctypes.winapi_abi,
+ ctypes.int32_t, /* success */
+ ctypes.void_t.ptr); /* handle */
+ CloseHandle(aHandle);
+ lib.close();
+}
+
+/**
+ * Creates a mutex.
+ *
+ * @param aName
+ * The name for the mutex.
+ * @return The Win32 handle to the mutex.
+ */
+function createMutex(aName) {
+ if (!IS_WIN) {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const INITIAL_OWN = 1;
+ const ERROR_ALREADY_EXISTS = 0xB7;
+ let lib = ctypes.open("kernel32.dll");
+ let CreateMutexW = lib.declare("CreateMutexW",
+ ctypes.winapi_abi,
+ ctypes.void_t.ptr, /* return handle */
+ ctypes.void_t.ptr, /* security attributes */
+ ctypes.int32_t, /* initial owner */
+ ctypes.char16_t.ptr); /* name */
+
+ let handle = CreateMutexW(null, INITIAL_OWN, aName);
+ lib.close();
+ let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
+ if (handle && !handle.isNull() && alreadyExists) {
+ closeHandle(handle);
+ handle = null;
+ }
+
+ if (handle && handle.isNull()) {
+ handle = null;
+ }
+
+ return handle;
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js
new file mode 100644
index 000000000..a0a95af1b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForDifferentChannel.js
@@ -0,0 +1,46 @@
+/* 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/.
+ */
+
+/* General Update Manager Tests */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing removal of an active update for a channel that is not" +
+ "valid due to switching channels (Bug 486275).");
+
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_DOWNLOADING);
+
+ patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_FAILED);
+ updates = getLocalUpdateString(patches, null, "Existing", "version 3.0",
+ "3.0", "3.0", null, null, null, null,
+ getString("patchApplyFailure"));
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), false);
+
+ setUpdateChannel("original_channel");
+
+ standardInit();
+
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager update count" + MSG_SHOULD_EQUAL);
+ let update = gUpdateManager.getUpdateAt(0);
+ Assert.equal(update.name, "Existing",
+ "the update's name" + MSG_SHOULD_EQUAL);
+
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "there should not be an active update");
+ // Verify that the active-update.xml file has had the update from the old
+ // channel removed.
+ let file = getUpdatesXMLFile(true);
+ Assert.equal(readFile(file), getLocalUpdatesXMLString(""),
+ "the contents of active-update.xml" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js
new file mode 100644
index 000000000..fc4f09787
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForOlderAppVersion.js
@@ -0,0 +1,29 @@
+/* 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/.
+ */
+
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing cleanup of an update download in progress for an " +
+ "older version of the application on startup (Bug 485624)");
+
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, "version 0.9", "0.9");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_DOWNLOADING);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+
+ standardInit();
+
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "there should not be an active update");
+ Assert.equal(gUpdateManager.updateCount, 0,
+ "the update manager update count" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js
new file mode 100644
index 000000000..b2d8ecbc6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingForSameVersionAndBuildID.js
@@ -0,0 +1,30 @@
+/* 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/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing removal of an update download in progress for the " +
+ "same version of the application with the same application " +
+ "build id on startup (Bug 536547)");
+
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0",
+ "2007010101");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_DOWNLOADING);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+
+ standardInit();
+
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "there should not be an active update");
+ Assert.equal(gUpdateManager.updateCount, 0,
+ "the update manager update count" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js
new file mode 100644
index 000000000..13e4aeaf6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupDownloadingIncorrectStatus.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing update cleanup when reading the status file returns " +
+ "STATUS_NONE and the update xml has an update with " +
+ "STATE_DOWNLOADING (Bug 539717).");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_NONE);
+
+ standardInit();
+
+ let dir = getUpdatesDir();
+ dir.append(DIR_PATCH);
+ Assert.ok(dir.exists(), MSG_SHOULD_EXIST);
+
+ let statusFile = dir.clone();
+ statusFile.append(FILE_UPDATE_STATUS);
+ Assert.ok(!statusFile.exists(), MSG_SHOULD_NOT_EXIST);
+
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "there should not be an active update");
+ Assert.equal(gUpdateManager.updateCount, 0,
+ "the update manager update count" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js
new file mode 100644
index 000000000..7661da82d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupPendingVersionFileIncorrectStatus.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing update cleanup when reading the status file returns " +
+ "STATUS_NONE, the version file is for a newer version, and the " +
+ "update xml has an update with STATE_PENDING (Bug 601701).");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeVersionFile("99.9");
+
+ standardInit();
+
+ // Check that there is no activeUpdate first so the updates directory is
+ // cleaned up by the UpdateManager before the remaining tests.
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "there should not be an active update");
+ Assert.equal(gUpdateManager.updateCount, 0,
+ "the update manager update count" + MSG_SHOULD_EQUAL);
+
+ let dir = getUpdatesDir();
+ dir.append(DIR_PATCH);
+ Assert.ok(dir.exists(), MSG_SHOULD_EXIST);
+
+ let versionFile = dir.clone();
+ versionFile.append(FILE_UPDATE_VERSION);
+ Assert.ok(!versionFile.exists(), MSG_SHOULD_NOT_EXIST);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js
new file mode 100644
index 000000000..d683b9931
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogMove.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing that the update.log is moved after a successful update");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ writeFile(log, "Last Update Log");
+
+ standardInit();
+
+ Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST);
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(), MSG_SHOULD_EXIST);
+ Assert.equal(readFile(log), "Last Update Log",
+ "the last update log contents" + MSG_SHOULD_EQUAL);
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST);
+
+ let dir = getUpdatesPatchDir();
+ Assert.ok(dir.exists(), MSG_SHOULD_EXIST);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js
new file mode 100644
index 000000000..8be93d0ff
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/cleanupSuccessLogsFIFO.js
@@ -0,0 +1,45 @@
+/* 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/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing update logs are first in first out deleted");
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_PENDING);
+ let updates = getLocalUpdateString(patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ let log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ writeFile(log, "Backup Update Log");
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ writeFile(log, "To Be Deleted Backup Update Log");
+
+ log = getUpdateLog(FILE_UPDATE_LOG);
+ writeFile(log, "Last Update Log");
+
+ standardInit();
+
+ Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST);
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(), MSG_SHOULD_EXIST);
+ Assert.equal(readFile(log), "Last Update Log",
+ "the last update log contents" + MSG_SHOULD_EQUAL);
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(log.exists(), MSG_SHOULD_EXIST);
+ Assert.equal(readFile(log), "Backup Update Log",
+ "the backup update log contents" + MSG_SHOULD_EQUAL);
+
+ let dir = getUpdatesPatchDir();
+ Assert.ok(dir.exists(), MSG_SHOULD_EXIST);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js
new file mode 100644
index 000000000..b715fb56e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadAndHashCheckMar.js
@@ -0,0 +1,161 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var gNextRunFunc;
+var gExpectedStatusResult;
+
+function run_test() {
+ // The network code that downloads the mar file accesses the profile to cache
+ // the download, but the profile is only available after calling
+ // do_get_profile in xpcshell tests. This prevents an error from being logged.
+ do_get_profile();
+
+ setupTestCommon();
+
+ debugDump("testing mar download and mar hash verification");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
+ start_httpserver();
+ setUpdateURL(gURLData + gHTTPHandlerPath);
+ standardInit();
+ // Only perform the non hash check tests when mar signing is enabled since the
+ // update service doesn't perform hash checks when mar signing is enabled.
+ if (MOZ_VERIFY_MAR_SIGNATURE) {
+ do_execute_soon(run_test_pt11);
+ } else {
+ do_execute_soon(run_test_pt1);
+ }
+}
+
+// The HttpServer must be stopped before calling do_test_finished
+function finish_test() {
+ stop_httpserver(doTestFinish);
+}
+
+// Helper function for testing mar downloads that have the correct size
+// specified in the update xml.
+function run_test_helper_pt1(aMsg, aExpectedStatusResult, aNextRunFunc) {
+ gUpdates = null;
+ gUpdateCount = null;
+ gStatusResult = null;
+ gCheckFunc = check_test_helper_pt1_1;
+ gNextRunFunc = aNextRunFunc;
+ gExpectedStatusResult = aExpectedStatusResult;
+ debugDump(aMsg, Components.stack.caller);
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_helper_pt1_1() {
+ Assert.equal(gUpdateCount, 1,
+ "the update count" + MSG_SHOULD_EQUAL);
+ gCheckFunc = check_test_helper_pt1_2;
+ let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount);
+ let state = gAUS.downloadUpdate(bestUpdate, false);
+ if (state == STATE_NONE || state == STATE_FAILED) {
+ do_throw("nsIApplicationUpdateService:downloadUpdate returned " + state);
+ }
+ gAUS.addDownloadListener(downloadListener);
+}
+
+function check_test_helper_pt1_2() {
+ Assert.equal(gStatusResult, gExpectedStatusResult,
+ "the download status result" + MSG_SHOULD_EQUAL);
+ gAUS.removeDownloadListener(downloadListener);
+ gNextRunFunc();
+}
+
+function setResponseBody(aHashFunction, aHashValue, aSize) {
+ let patches = getRemotePatchString(null, null,
+ aHashFunction, aHashValue, aSize);
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+}
+
+// mar download with a valid MD5 hash
+function run_test_pt1() {
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with a valid MD5 hash",
+ Cr.NS_OK, run_test_pt2);
+}
+
+// mar download with an invalid MD5 hash
+function run_test_pt2() {
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR + "0");
+ run_test_helper_pt1("mar download with an invalid MD5 hash",
+ Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt3);
+}
+
+// mar download with a valid SHA1 hash
+function run_test_pt3() {
+ setResponseBody("SHA1", SHA1_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with a valid SHA1 hash",
+ Cr.NS_OK, run_test_pt4);
+}
+
+// mar download with an invalid SHA1 hash
+function run_test_pt4() {
+ setResponseBody("SHA1", SHA1_HASH_SIMPLE_MAR + "0");
+ run_test_helper_pt1("mar download with an invalid SHA1 hash",
+ Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt5);
+}
+
+// mar download with a valid SHA256 hash
+function run_test_pt5() {
+ setResponseBody("SHA256", SHA256_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with a valid SHA256 hash",
+ Cr.NS_OK, run_test_pt6);
+}
+
+// mar download with an invalid SHA256 hash
+function run_test_pt6() {
+ setResponseBody("SHA256", SHA256_HASH_SIMPLE_MAR + "0");
+ run_test_helper_pt1("mar download with an invalid SHA256 hash",
+ Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt7);
+}
+
+// mar download with a valid SHA384 hash
+function run_test_pt7() {
+ setResponseBody("SHA384", SHA384_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with a valid SHA384 hash",
+ Cr.NS_OK, run_test_pt8);
+}
+
+// mar download with an invalid SHA384 hash
+function run_test_pt8() {
+ setResponseBody("SHA384", SHA384_HASH_SIMPLE_MAR + "0");
+ run_test_helper_pt1("mar download with an invalid SHA384 hash",
+ Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt9);
+}
+
+// mar download with a valid SHA512 hash
+function run_test_pt9() {
+ setResponseBody("SHA512", SHA512_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with a valid SHA512 hash",
+ Cr.NS_OK, run_test_pt10);
+}
+
+// mar download with an invalid SHA512 hash
+function run_test_pt10() {
+ setResponseBody("SHA512", SHA512_HASH_SIMPLE_MAR + "0");
+ run_test_helper_pt1("mar download with an invalid SHA512 hash",
+ Cr.NS_ERROR_CORRUPTED_CONTENT, run_test_pt11);
+}
+
+// mar download with the mar not found
+function run_test_pt11() {
+ let patches = getRemotePatchString(null, gURLData + "missing.mar");
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("mar download with the mar not found",
+ Cr.NS_ERROR_UNEXPECTED, run_test_pt12);
+}
+
+// mar download with a valid MD5 hash but invalid file size
+function run_test_pt12() {
+ const arbitraryFileSize = 1024000;
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR, arbitraryFileSize);
+ run_test_helper_pt1("mar download with a valid MD5 hash but invalid file size",
+ Cr.NS_ERROR_UNEXPECTED, finish_test);
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js
new file mode 100644
index 000000000..159033792
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadCompleteAfterPartialFailure.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+const WindowWatcher = {
+ getNewPrompter: function WW_getNewPrompter(aParent) {
+ Assert.ok(!aParent,
+ "the aParent parameter should not be defined");
+ return {
+ alert: function WW_GNP_alert(aTitle, aText) {
+ let title = getString("updaterIOErrorTitle");
+ Assert.equal(aTitle, title,
+ "the ui string for title" + MSG_SHOULD_EQUAL);
+ let text = gUpdateBundle.formatStringFromName("updaterIOErrorMsg",
+ [Services.appinfo.name,
+ Services.appinfo.name], 2);
+ Assert.equal(aText, text,
+ "the ui string for message" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+ }
+ };
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher])
+};
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing download a complete on partial failure. Calling " +
+ "nsIUpdatePrompt::showUpdateError should call getNewPrompter " +
+ "and alert on the object returned by getNewPrompter when the " +
+ "update.state == " + STATE_FAILED + " and the update.errorCode " +
+ "== " + WRITE_ERROR + " (Bug 595059).");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+
+ let windowWatcherCID =
+ MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+ WindowWatcher);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ });
+
+ standardInit();
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let url = URL_HOST + "/" + FILE_COMPLETE_MAR;
+ let patches = getLocalPatchString("complete", url, null, null, null, null,
+ STATE_FAILED);
+ let updates = getLocalUpdateString(patches, null, null, "version 1.0", "1.0",
+ null, null, null, null, url);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_FAILED);
+
+ reloadUpdateManagerData();
+
+ let update = gUpdateManager.activeUpdate;
+ update.errorCode = WRITE_ERROR;
+ let prompter = Cc["@mozilla.org/updates/update-prompt;1"].
+ createInstance(Ci.nsIUpdatePrompt);
+ prompter.showUpdateError(update);
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js
new file mode 100644
index 000000000..ef2da26af
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadInterruptedRecovery.js
@@ -0,0 +1,225 @@
+/* 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/.
+ */
+
+/* General MAR File Download Tests */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+const INC_CONTRACT_ID = "@mozilla.org/network/incremental-download;1";
+
+// gIncrementalDownloadErrorType is used to loop through each of the connection
+// error types in the Mock incremental downloader.
+var gIncrementalDownloadErrorType = 0;
+
+var gNextRunFunc;
+var gExpectedStatusResult;
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing mar downloads, mar hash verification, and " +
+ "mar download interrupted recovery");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_STAGING_ENABLED, false);
+ start_httpserver();
+ setUpdateURL(gURLData + gHTTPHandlerPath);
+ standardInit();
+ do_execute_soon(run_test_pt1);
+}
+
+// The HttpServer must be stopped before calling do_test_finished
+function finish_test() {
+ stop_httpserver(doTestFinish);
+}
+
+// Helper function for testing mar downloads that have the correct size
+// specified in the update xml.
+function run_test_helper_pt1(aMsg, aExpectedStatusResult, aNextRunFunc) {
+ gUpdates = null;
+ gUpdateCount = null;
+ gStatusResult = null;
+ gCheckFunc = check_test_helper_pt1_1;
+ gNextRunFunc = aNextRunFunc;
+ gExpectedStatusResult = aExpectedStatusResult;
+ debugDump(aMsg, Components.stack.caller);
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_helper_pt1_1() {
+ Assert.equal(gUpdateCount, 1,
+ "the update count" + MSG_SHOULD_EQUAL);
+ gCheckFunc = check_test_helper_pt1_2;
+ let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount);
+ let state = gAUS.downloadUpdate(bestUpdate, false);
+ if (state == STATE_NONE || state == STATE_FAILED) {
+ do_throw("nsIApplicationUpdateService:downloadUpdate returned " + state);
+ }
+ gAUS.addDownloadListener(downloadListener);
+}
+
+function check_test_helper_pt1_2() {
+ Assert.equal(gStatusResult, gExpectedStatusResult,
+ "the download status result" + MSG_SHOULD_EQUAL);
+ gAUS.removeDownloadListener(downloadListener);
+ gNextRunFunc();
+}
+
+function setResponseBody(aHashFunction, aHashValue, aSize) {
+ let patches = getRemotePatchString(null, null,
+ aHashFunction, aHashValue, aSize);
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+}
+
+function initMockIncrementalDownload() {
+ let incrementalDownloadCID =
+ MockRegistrar.register(INC_CONTRACT_ID, IncrementalDownload);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(incrementalDownloadCID);
+ });
+}
+
+/* This Mock incremental downloader is used to verify that connection
+ * interrupts work correctly in updater code. The implementation of
+ * the mock incremental downloader is very simple, it simply copies
+ * the file to the destination location.
+ */
+
+function IncrementalDownload() {
+ this.wrappedJSObject = this;
+}
+
+IncrementalDownload.prototype = {
+ /* nsIIncrementalDownload */
+ init: function(uri, file, chunkSize, intervalInSeconds) {
+ this._destination = file;
+ this._URI = uri;
+ this._finalURI = uri;
+ },
+
+ start: function(observer, ctxt) {
+ let tm = Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager);
+ // Do the actual operation async to give a chance for observers
+ // to add themselves.
+ tm.mainThread.dispatch(function() {
+ this._observer = observer.QueryInterface(Ci.nsIRequestObserver);
+ this._ctxt = ctxt;
+ this._observer.onStartRequest(this, this._ctxt);
+ let mar = getTestDirFile(FILE_SIMPLE_MAR);
+ mar.copyTo(this._destination.parent, this._destination.leafName);
+ let status = Cr.NS_OK;
+ switch (gIncrementalDownloadErrorType++) {
+ case 0:
+ status = Cr.NS_ERROR_NET_RESET;
+ break;
+ case 1:
+ status = Cr.NS_ERROR_CONNECTION_REFUSED;
+ break;
+ case 2:
+ status = Cr.NS_ERROR_NET_RESET;
+ break;
+ case 3:
+ status = Cr.NS_OK;
+ break;
+ case 4:
+ status = Cr.NS_ERROR_OFFLINE;
+ // After we report offline, we want to eventually show offline
+ // status being changed to online.
+ let tm2 = Cc["@mozilla.org/thread-manager;1"].
+ getService(Ci.nsIThreadManager);
+ tm2.mainThread.dispatch(function() {
+ Services.obs.notifyObservers(gAUS,
+ "network:offline-status-changed",
+ "online");
+ }, Ci.nsIThread.DISPATCH_NORMAL);
+ break;
+ }
+ this._observer.onStopRequest(this, this._ctxt, status);
+ }.bind(this), Ci.nsIThread.DISPATCH_NORMAL);
+ },
+
+ get URI() {
+ return this._URI;
+ },
+
+ get currentSize() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ get destination() {
+ return this._destination;
+ },
+
+ get finalURI() {
+ return this._finalURI;
+ },
+
+ get totalSize() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+
+ /* nsIRequest */
+ cancel: function(aStatus) {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ suspend: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ isPending: function() {
+ throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+ },
+ _loadFlags: 0,
+ get loadFlags() {
+ return this._loadFlags;
+ },
+ set loadFlags(val) {
+ this._loadFlags = val;
+ },
+
+ _loadGroup: null,
+ get loadGroup() {
+ return this._loadGroup;
+ },
+ set loadGroup(val) {
+ this._loadGroup = val;
+ },
+
+ _name: "",
+ get name() {
+ return this._name;
+ },
+
+ _status: 0,
+ get status() {
+ return this._status;
+ },
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIIncrementalDownload])
+};
+
+// Test disconnecting during an update
+function run_test_pt1() {
+ initMockIncrementalDownload();
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with connection interruption",
+ Cr.NS_OK, run_test_pt2);
+}
+
+// Test disconnecting during an update
+function run_test_pt2() {
+ gIncrementalDownloadErrorType = 0;
+ Services.prefs.setIntPref(PREF_APP_UPDATE_SOCKET_MAXERRORS, 2);
+ Services.prefs.setIntPref(PREF_APP_UPDATE_RETRYTIMEOUT, 0);
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with connection interruption without recovery",
+ Cr.NS_ERROR_NET_RESET, run_test_pt3);
+}
+
+// Test entering offline mode while downloading
+function run_test_pt3() {
+ gIncrementalDownloadErrorType = 4;
+ setResponseBody("MD5", MD5_HASH_SIMPLE_MAR);
+ run_test_helper_pt1("mar download with offline mode",
+ Cr.NS_OK, finish_test);
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js
new file mode 100644
index 000000000..ca065f573
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/downloadResumeForSameAppVersion.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing resuming an update download in progress for the same " +
+ "version of the application on startup (Bug 485624)");
+
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_DOWNLOADING);
+ let updates = getLocalUpdateString(patches, null, null, "1.0", "1.0");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_DOWNLOADING);
+
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+
+ standardInit();
+
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.activeUpdate.state, STATE_DOWNLOADING,
+ "the update manager activeUpdate state attribute" +
+ MSG_SHOULD_EQUAL);
+
+ // Pause the download and reload the Update Manager with an empty update so
+ // the Application Update Service doesn't write the update xml files during
+ // xpcom-shutdown which will leave behind the test directory.
+ gAUS.pauseDownload();
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ reloadUpdateManagerData();
+
+ do_execute_soon(doTestFinish);
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/head_update.js b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js
new file mode 100644
index 000000000..9715c5828
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/head_update.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const IS_SERVICE_TEST = false;
+
+/* import-globals-from ../data/xpcshellUtilsAUS.js */
+load("../data/xpcshellUtilsAUS.js");
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js
new file mode 100644
index 000000000..831c87257
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/remoteUpdateXML.js
@@ -0,0 +1,285 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+var gNextRunFunc;
+var gExpectedCount;
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing remote update xml attributes");
+
+ start_httpserver();
+ setUpdateURL(gURLData + gHTTPHandlerPath);
+ setUpdateChannel("test_channel");
+
+ // This test expects that the app.update.download.backgroundInterval
+ // preference doesn't already exist.
+ Services.prefs.deleteBranch("app.update.download.backgroundInterval");
+
+ standardInit();
+ do_execute_soon(run_test_pt01);
+}
+
+// Helper function for testing update counts returned from an update xml
+function run_test_helper_pt1(aMsg, aExpectedCount, aNextRunFunc) {
+ gUpdates = null;
+ gUpdateCount = null;
+ gCheckFunc = check_test_helper_pt1;
+ gNextRunFunc = aNextRunFunc;
+ gExpectedCount = aExpectedCount;
+ debugDump(aMsg, Components.stack.caller);
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_helper_pt1() {
+ Assert.equal(gUpdateCount, gExpectedCount,
+ "the update count" + MSG_SHOULD_EQUAL);
+ gNextRunFunc();
+}
+
+// update xml not found
+function run_test_pt01() {
+ run_test_helper_pt1("testing update xml not available",
+ null, run_test_pt02);
+}
+
+// one update available and the update's property values
+function run_test_pt02() {
+ debugDump("testing one update available and the update's property values");
+ gUpdates = null;
+ gUpdateCount = null;
+ gCheckFunc = check_test_pt02;
+ let patches = getRemotePatchString("complete", "http://complete/", "SHA1",
+ "98db9dad8e1d80eda7e1170d0187d6f53e477059",
+ "9856459");
+ patches += getRemotePatchString("partial", "http://partial/", "SHA1",
+ "e6678ca40ae7582316acdeddf3c133c9c8577de4",
+ "1316138");
+ let updates = getRemoteUpdateString(patches, "minor", "Minor Test",
+ "version 2.1a1pre", "2.1a1pre",
+ "20080811053724",
+ "http://details/",
+ "true",
+ "true", "345600", "1200",
+ "custom1_attr=\"custom1 value\"",
+ "custom2_attr=\"custom2 value\"");
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_pt02() {
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+ // and until this is fixed this will not test the value for detailsURL when it
+ // isn't specified in the update xml.
+// let defaultDetailsURL;
+// try {
+ // Try using a default details URL supplied by the distribution
+ // if the update XML does not supply one.
+// let formatter = Cc["@mozilla.org/toolkit/URLFormatterService;1"].
+// getService(Ci.nsIURLFormatter);
+// defaultDetailsURL = formatter.formatURLPref(PREF_APP_UPDATE_URL_DETAILS);
+// } catch (e) {
+// defaultDetailsURL = "";
+// }
+
+ Assert.equal(gUpdateCount, 1,
+ "the update count" + MSG_SHOULD_EQUAL);
+ let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount).QueryInterface(Ci.nsIPropertyBag);
+ Assert.equal(bestUpdate.type, "minor",
+ "the update type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.name, "Minor Test",
+ "the update name attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.displayVersion, "version 2.1a1pre",
+ "the update displayVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.appVersion, "2.1a1pre",
+ "the update appVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.buildID, "20080811053724",
+ "the update buildID attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.detailsURL, "http://details/",
+ "the update detailsURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(bestUpdate.showPrompt,
+ "the update showPrompt attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(bestUpdate.showNeverForVersion,
+ "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.promptWaitTime, "345600",
+ "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL);
+ // The default and maximum value for backgroundInterval is 600
+ Assert.equal(bestUpdate.getProperty("backgroundInterval"), "600",
+ "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.serviceURL, gURLData + gHTTPHandlerPath + "?force=1",
+ "the update serviceURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.channel, "test_channel",
+ "the update channel attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!bestUpdate.isCompleteUpdate,
+ "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!bestUpdate.isSecurityUpdate,
+ "the update isSecurityUpdate attribute" + MSG_SHOULD_EQUAL);
+ // Check that installDate is within 10 seconds of the current date.
+ Assert.ok((Date.now() - bestUpdate.installDate) < 10000,
+ "the update installDate attribute should be within 10 seconds of " +
+ "the current time");
+ Assert.ok(!bestUpdate.statusText,
+ "the update statusText attribute" + MSG_SHOULD_EQUAL);
+ // nsIUpdate:state returns an empty string when no action has been performed
+ // on an available update
+ Assert.equal(bestUpdate.state, "",
+ "the update state attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.errorCode, 0,
+ "the update errorCode attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.patchCount, 2,
+ "the update patchCount attribute" + MSG_SHOULD_EQUAL);
+ // XXX TODO - test nsIUpdate:serialize
+
+ Assert.equal(bestUpdate.getProperty("custom1_attr"), "custom1 value",
+ "the update custom1_attr property" + MSG_SHOULD_EQUAL);
+ Assert.equal(bestUpdate.getProperty("custom2_attr"), "custom2 value",
+ "the update custom2_attr property" + MSG_SHOULD_EQUAL);
+
+ let patch = bestUpdate.getPatchAt(0);
+ Assert.equal(patch.type, "complete",
+ "the update patch type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.URL, "http://complete/",
+ "the update patch URL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashFunction, "SHA1",
+ "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashValue, "98db9dad8e1d80eda7e1170d0187d6f53e477059",
+ "the update patch hashValue attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.size, 9856459,
+ "the update patch size attribute" + MSG_SHOULD_EQUAL);
+ // The value for patch.state can be the string 'null' as a valid value. This
+ // is confusing if it returns null which is an invalid value since the test
+ // failure output will show a failure for null == null. To lessen the
+ // confusion first check that the typeof for patch.state is string.
+ Assert.equal(typeof patch.state, "string",
+ "the update patch state typeof value should equal |string|");
+ Assert.equal(patch.state, STATE_NONE,
+ "the update patch state attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!patch.selected,
+ "the update patch selected attribute" + MSG_SHOULD_EQUAL);
+ // XXX TODO - test nsIUpdatePatch:serialize
+
+ patch = bestUpdate.getPatchAt(1);
+ Assert.equal(patch.type, "partial",
+ "the update patch type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.URL, "http://partial/",
+ "the update patch URL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashFunction, "SHA1",
+ "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashValue, "e6678ca40ae7582316acdeddf3c133c9c8577de4",
+ "the update patch hashValue attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.size, 1316138,
+ "the update patch size attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.state, STATE_NONE,
+ "the update patch state attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!patch.selected,
+ "the update patch selected attribute" + MSG_SHOULD_EQUAL);
+ // XXX TODO - test nsIUpdatePatch:serialize
+
+ run_test_pt03();
+}
+
+// Empty update xml (an empty xml file returns a root node name of parsererror)
+function run_test_pt03() {
+ gResponseBody = "<parsererror/>";
+ run_test_helper_pt1("testing empty update xml",
+ null, run_test_pt04);
+}
+
+// no updates available
+function run_test_pt04() {
+ gResponseBody = getRemoteUpdatesXMLString("");
+ run_test_helper_pt1("testing no updates available",
+ 0, run_test_pt05);
+}
+
+// one update available with two patches
+function run_test_pt05() {
+ let patches = getRemotePatchString("complete");
+ patches += getRemotePatchString("partial");
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing one update available",
+ 1, run_test_pt06);
+}
+
+// three updates available each with two patches
+function run_test_pt06() {
+ let patches = getRemotePatchString("complete");
+ patches += getRemotePatchString("partial");
+ let updates = getRemoteUpdateString(patches);
+ updates += getRemoteUpdateString(patches);
+ updates += getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing three updates available",
+ 3, run_test_pt07);
+}
+
+// one update with complete and partial patches with size 0 specified in the
+// update xml
+function run_test_pt07() {
+ let patches = getRemotePatchString("complete", null, null, null, "0");
+ patches += getRemotePatchString("partial", null, null, null, "0");
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing one update with complete and partial " +
+ "patches with size 0", 0, run_test_pt08);
+}
+
+// one update with complete patch with size 0 specified in the update xml
+function run_test_pt08() {
+ let patches = getRemotePatchString("complete", null, null, null, "0");
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing one update with complete patch with size 0",
+ 0, run_test_pt9);
+}
+
+// one update with partial patch with size 0 specified in the update xml
+function run_test_pt9() {
+ let patches = getRemotePatchString("partial", null, null, null, "0");
+ let updates = getRemoteUpdateString(patches);
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing one update with partial patch with size 0",
+ 0, run_test_pt10);
+}
+
+// check that updates for older versions of the application aren't selected
+function run_test_pt10() {
+ let patches = getRemotePatchString("complete");
+ patches += getRemotePatchString("partial");
+ let updates = getRemoteUpdateString(patches, "minor", null, null, "1.0pre");
+ updates += getRemoteUpdateString(patches, "minor", null, null, "1.0a");
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing two updates older than the current version",
+ 2, check_test_pt10);
+}
+
+function check_test_pt10() {
+ let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount);
+ Assert.ok(!bestUpdate,
+ "there should be no update available");
+ run_test_pt11();
+}
+
+// check that updates for the current version of the application are selected
+function run_test_pt11() {
+ let patches = getRemotePatchString("complete");
+ patches += getRemotePatchString("partial");
+ let updates = getRemoteUpdateString(patches, "minor", null, "version 1.0");
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ run_test_helper_pt1("testing one update equal to the current version",
+ 1, check_test_pt11);
+}
+
+function check_test_pt11() {
+ let bestUpdate = gAUS.selectUpdate(gUpdates, gUpdateCount);
+ Assert.ok(!!bestUpdate,
+ "there should be one update available");
+ Assert.equal(bestUpdate.displayVersion, "version 1.0",
+ "the update displayVersion attribute" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js b/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js
new file mode 100644
index 000000000..ee1c40bfd
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/uiAutoPref.js
@@ -0,0 +1,75 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+const WindowWatcher = {
+ openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) {
+ gCheckFunc();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher])
+};
+
+const WindowMediator = {
+ getMostRecentWindow: function(aWindowType) {
+ do_execute_soon(check_status);
+ return { getInterface: XPCOMUtils.generateQI([Ci.nsIDOMWindow]) };
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediator])
+};
+
+function run_test() {
+ setupTestCommon();
+ // Calling do_get_profile prevents an error from being logged
+ do_get_profile();
+
+ debugDump("testing that an update download doesn't start when the " +
+ PREF_APP_UPDATE_AUTO + " preference is false");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, false);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+
+ start_httpserver();
+ setUpdateURL(gURLData + gHTTPHandlerPath);
+ standardInit();
+
+ let windowWatcherCID =
+ MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+ WindowWatcher);
+ let windowMediatorCID =
+ MockRegistrar.register("@mozilla.org/appshell/window-mediator;1",
+ WindowMediator);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ MockRegistrar.unregister(windowMediatorCID);
+ });
+
+ gCheckFunc = check_showUpdateAvailable;
+ let patches = getRemotePatchString("complete");
+ let updates = getRemoteUpdateString(patches, "minor", null, null, "1.0");
+ gResponseBody = getRemoteUpdatesXMLString(updates);
+ gAUS.notify(null);
+}
+
+function check_status() {
+ let status = readStatusFile();
+ Assert.notEqual(status, STATE_DOWNLOADING,
+ "the update state" + MSG_SHOULD_EQUAL);
+
+ // Pause the download and reload the Update Manager with an empty update so
+ // the Application Update Service doesn't write the update xml files during
+ // xpcom-shutdown which will leave behind the test directory.
+ gAUS.pauseDownload();
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), true);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ reloadUpdateManagerData();
+
+ do_execute_soon(doTestFinish);
+}
+
+function check_showUpdateAvailable() {
+ do_throw("showUpdateAvailable should not have called openWindow!");
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js b/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js
new file mode 100644
index 000000000..25110be8c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/uiSilentPref.js
@@ -0,0 +1,76 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+/**
+ * Test that nsIUpdatePrompt doesn't display UI for showUpdateAvailable and
+ * showUpdateError when the app.update.silent preference is true.
+ */
+
+const WindowWatcher = {
+ openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) {
+ gCheckFunc();
+ },
+
+ getNewPrompter: function(aParent) {
+ gCheckFunc();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher])
+};
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing nsIUpdatePrompt notifications should not be seen " +
+ "when the " + PREF_APP_UPDATE_SILENT + " preference is true");
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, true);
+
+ let windowWatcherCID =
+ MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+ WindowWatcher);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ });
+
+ standardInit();
+
+ debugDump("testing showUpdateAvailable should not call openWindow");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(""), false);
+ let patches = getLocalPatchString(null, null, null, null, null, null,
+ STATE_FAILED);
+ let updates = getLocalUpdateString(patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_FAILED);
+ reloadUpdateManagerData();
+
+ gCheckFunc = check_showUpdateAvailable;
+ let update = gUpdateManager.activeUpdate;
+ gUP.showUpdateAvailable(update);
+ // Report a successful check after the call to showUpdateAvailable since it
+ // didn't throw and otherwise it would report no tests run.
+ Assert.ok(true,
+ "calling showUpdateAvailable should not attempt to open a window");
+
+ debugDump("testing showUpdateError should not call getNewPrompter");
+ gCheckFunc = check_showUpdateError;
+ update.errorCode = WRITE_ERROR;
+ gUP.showUpdateError(update);
+ // Report a successful check after the call to showUpdateError since it
+ // didn't throw and otherwise it would report no tests run.
+ Assert.ok(true,
+ "calling showUpdateError should not attempt to open a window");
+
+ doTestFinish();
+}
+
+function check_showUpdateAvailable() {
+ do_throw("showUpdateAvailable should not have called openWindow!");
+}
+
+function check_showUpdateError() {
+ do_throw("showUpdateError should not have seen getNewPrompter!");
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js b/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js
new file mode 100644
index 000000000..5b694ed30
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/uiUnsupportedAlreadyNotified.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+Cu.import("resource://testing-common/MockRegistrar.jsm");
+
+const WindowWatcher = {
+ openWindow: function(aParent, aUrl, aName, aFeatures, aArgs) {
+ check_showUpdateAvailable();
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowWatcher])
+};
+
+const WindowMediator = {
+ getMostRecentWindow: function(aWindowType) {
+ return null;
+ },
+
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIWindowMediator])
+};
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing nsIUpdatePrompt notifications should not be displayed " +
+ "when showUpdateAvailable is called for an unsupported system " +
+ "update when the unsupported notification has already been " +
+ "shown (bug 843497)");
+
+ start_httpserver();
+ setUpdateURL(gURLData + gHTTPHandlerPath);
+ standardInit();
+
+ let windowWatcherCID =
+ MockRegistrar.register("@mozilla.org/embedcomp/window-watcher;1",
+ WindowWatcher);
+ let windowMediatorCID =
+ MockRegistrar.register("@mozilla.org/appshell/window-mediator;1",
+ WindowMediator);
+ do_register_cleanup(() => {
+ MockRegistrar.unregister(windowWatcherCID);
+ MockRegistrar.unregister(windowMediatorCID);
+ });
+
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_SILENT, false);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_NOTIFIEDUNSUPPORTED, true);
+ // This preference is used to determine when the background update check has
+ // completed since a successful check will clear the preference.
+ Services.prefs.setIntPref(PREF_APP_UPDATE_BACKGROUNDERRORS, 1);
+
+ gResponseBody = getRemoteUpdatesXMLString(" <update type=\"major\" " +
+ "name=\"Unsupported Update\" " +
+ "unsupported=\"true\" " +
+ "detailsURL=\"" + URL_HOST +
+ "\"></update>\n");
+ gAUS.notify(null);
+ do_execute_soon(check_test);
+}
+
+function check_test() {
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_BACKGROUNDERRORS)) {
+ do_execute_soon(check_test);
+ return;
+ }
+ Assert.ok(true,
+ PREF_APP_UPDATE_BACKGROUNDERRORS + " preference should not exist");
+
+ stop_httpserver(doTestFinish);
+}
+
+function check_showUpdateAvailable() {
+ do_throw("showUpdateAvailable should not have called openWindow!");
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js
new file mode 100644
index 000000000..e46469455
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/updateManagerXML.js
@@ -0,0 +1,177 @@
+/* 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/.
+ */
+
+function run_test() {
+ setupTestCommon();
+
+ debugDump("testing addition of a successful update to " + FILE_UPDATES_XML +
+ " and verification of update properties including the format " +
+ "prior to bug 530872");
+
+ setUpdateChannel("test_channel");
+
+ // This test expects that the app.update.download.backgroundInterval
+ // preference doesn't already exist.
+ Services.prefs.deleteBranch("app.update.download.backgroundInterval");
+
+ // XXXrstrong - not specifying a detailsURL will cause a leak due to bug 470244
+ // and until bug 470244 is fixed this will not test the value for detailsURL
+ // when it isn't specified in the update xml.
+ let patches = getLocalPatchString("partial", "http://partial/", "SHA256",
+ "cd43", "86", "true", STATE_PENDING);
+ let updates = getLocalUpdateString(patches, "major", "New", "version 4",
+ "4.0", "20070811053724",
+ "http://details1/",
+ "http://service1/", "1238441300314",
+ "test status text", "false",
+ "test_channel", "true", "true", "true",
+ "345600", "300", "3.0",
+ "custom1_attr=\"custom1 value\"",
+ "custom2_attr=\"custom2 value\"");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeStatusFile(STATE_SUCCEEDED);
+
+ patches = getLocalPatchString("complete", "http://complete/", "SHA1", "6232",
+ "75", "true", STATE_FAILED);
+ updates = getLocalUpdateString(patches, "major", "Existing", null, "3.0",
+ null,
+ "http://details2/",
+ "http://service2/", null,
+ getString("patchApplyFailure"), "true",
+ "test_channel", "false", null, null, "691200",
+ null, null,
+ "custom3_attr=\"custom3 value\"",
+ "custom4_attr=\"custom4 value\"");
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), false);
+
+ standardInit();
+
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the update manager activeUpdate attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 2,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+
+ debugDump("checking the activeUpdate properties");
+ let update = gUpdateManager.getUpdateAt(0).QueryInterface(Ci.nsIPropertyBag);
+ Assert.equal(update.state, STATE_SUCCEEDED,
+ "the update state attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.type, "major",
+ "the update type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.name, "New",
+ "the update name attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.displayVersion, "version 4",
+ "the update displayVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.appVersion, "4.0",
+ "the update appVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.buildID, "20070811053724",
+ "the update buildID attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.detailsURL, "http://details1/",
+ "the update detailsURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.serviceURL, "http://service1/",
+ "the update serviceURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.installDate, "1238441300314",
+ "the update installDate attribute" + MSG_SHOULD_EQUAL);
+ // statusText is updated
+ Assert.equal(update.statusText, getString("installSuccess"),
+ "the update statusText attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!update.isCompleteUpdate,
+ "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.channel, "test_channel",
+ "the update channel attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!!update.showPrompt,
+ "the update showPrompt attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!!update.showNeverForVersion,
+ "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.promptWaitTime, "345600",
+ "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.getProperty("backgroundInterval"), "300",
+ "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.previousAppVersion, "3.0",
+ "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL);
+ // Custom attributes
+ Assert.equal(update.getProperty("custom1_attr"), "custom1 value",
+ "the update custom1_attr property" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.getProperty("custom2_attr"), "custom2 value",
+ "the update custom2_attr property" + MSG_SHOULD_EQUAL);
+
+ debugDump("checking the activeUpdate patch properties");
+ let patch = update.selectedPatch;
+ Assert.equal(patch.type, "partial",
+ "the update patch type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.URL, "http://partial/",
+ "the update patch URL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashFunction, "SHA256",
+ "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashValue, "cd43",
+ "the update patch hashValue attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.size, "86",
+ "the update patch size attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!!patch.selected,
+ "the update patch selected attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.state, STATE_SUCCEEDED,
+ "the update patch state attribute" + MSG_SHOULD_EQUAL);
+
+ debugDump("checking the first update properties");
+ update = gUpdateManager.getUpdateAt(1).QueryInterface(Ci.nsIPropertyBag);
+ Assert.equal(update.state, STATE_FAILED,
+ "the update state attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.name, "Existing",
+ "the update name attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.type, "major",
+ "the update type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.displayVersion, "3.0",
+ "the update displayVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.appVersion, "3.0",
+ "the update appVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.detailsURL, "http://details2/",
+ "the update detailsURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.serviceURL, "http://service2/",
+ "the update serviceURL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.installDate, "1238441400314",
+ "the update installDate attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.statusText, getString("patchApplyFailure"),
+ "the update statusText attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.buildID, "20080811053724",
+ "the update buildID attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!!update.isCompleteUpdate,
+ "the update isCompleteUpdate attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.channel, "test_channel",
+ "the update channel attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!update.showPrompt,
+ "the update showPrompt attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!update.showNeverForVersion,
+ "the update showNeverForVersion attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.promptWaitTime, "691200",
+ "the update promptWaitTime attribute" + MSG_SHOULD_EQUAL);
+ // The default and maximum value for backgroundInterval is 600
+ Assert.equal(update.getProperty("backgroundInterval"), "600",
+ "the update backgroundInterval attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.previousAppVersion, null,
+ "the update previousAppVersion attribute" + MSG_SHOULD_EQUAL);
+ // Custom attributes
+ Assert.equal(update.getProperty("custom3_attr"), "custom3 value",
+ "the update custom3_attr property" + MSG_SHOULD_EQUAL);
+ Assert.equal(update.getProperty("custom4_attr"), "custom4 value",
+ "the update custom4_attr property" + MSG_SHOULD_EQUAL);
+
+ debugDump("checking the first update patch properties");
+ patch = update.selectedPatch;
+ Assert.equal(patch.type, "complete",
+ "the update patch type attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.URL, "http://complete/",
+ "the update patch URL attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashFunction, "SHA1",
+ "the update patch hashFunction attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.hashValue, "6232",
+ "the update patch hashValue attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.size, "75",
+ "the update patch size attribute" + MSG_SHOULD_EQUAL);
+ Assert.ok(!!patch.selected,
+ "the update patch selected attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(patch.state, STATE_FAILED,
+ "the update patch state attribute" + MSG_SHOULD_EQUAL);
+
+ doTestFinish();
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js
new file mode 100644
index 000000000..db7d90094
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/urlConstruction.js
@@ -0,0 +1,305 @@
+/* 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/.
+ */
+
+/* General URL Construction Tests */
+
+const URL_PREFIX = URL_HOST + "/";
+
+var gAppInfo;
+
+// Since gUpdateChecker.checkForUpdates uses XMLHttpRequest and XMLHttpRequest
+// can be slow it combines the checks whenever possible.
+function run_test() {
+ // This test needs access to omni.ja to read the update.locale file so don't
+ // use a custom directory for the application directory.
+ gUseTestAppDir = false;
+ setupTestCommon();
+
+ standardInit();
+ gAppInfo = Cc["@mozilla.org/xre/app-info;1"].
+ getService(Ci.nsIXULAppInfo).
+ QueryInterface(Ci.nsIXULRuntime);
+ do_execute_soon(run_test_pt1);
+}
+
+
+// url constructed with:
+// %PRODUCT%
+// %VERSION%
+// %BUILD_ID%
+// %BUILD_TARGET%
+// %LOCALE%
+// %CHANNEL%
+// %PLATFORM_VERSION%
+// %OS_VERSION%
+// %SYSTEM_CAPABILITIES%
+// %DISTRIBUTION%
+// %DISTRIBUTION_VERSION%
+function run_test_pt1() {
+ gCheckFunc = check_test_pt1;
+ // The code that gets the locale accesses the profile which is only available
+ // after calling do_get_profile in xpcshell tests. This prevents an error from
+ // being logged.
+ do_get_profile();
+
+ setUpdateChannel("test_channel");
+ gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_ID, "test_distro");
+ gDefaultPrefBranch.setCharPref(PREF_DISTRIBUTION_VERSION, "test_distro_version");
+
+ let url = URL_PREFIX + "%PRODUCT%/%VERSION%/%BUILD_ID%/%BUILD_TARGET%/" +
+ "%LOCALE%/%CHANNEL%/%PLATFORM_VERSION%/%OS_VERSION%/" +
+ "%SYSTEM_CAPABILITIES%/%DISTRIBUTION%/%DISTRIBUTION_VERSION%/" +
+ "updates.xml";
+ debugDump("testing url construction - url: " + url);
+ setUpdateURL(url);
+ try {
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+ } catch (e) {
+ debugDump("The following error is most likely due to a missing " +
+ "update.locale file");
+ do_throw(e);
+ }
+}
+
+function check_test_pt1() {
+ let url = URL_PREFIX + gAppInfo.name + "/" + gAppInfo.version + "/" +
+ gAppInfo.appBuildID + "/" + gAppInfo.OS + "_" + getABI() + "/" +
+ INSTALL_LOCALE + "/test_channel/" + gAppInfo.platformVersion + "/" +
+ getOSVersion() + "/" + getSystemCapabilities() +
+ "/test_distro/test_distro_version/updates.xml?force=1";
+ // Log the urls since Assert.equal won't print the entire urls to the log.
+ if (gRequestURL != url) {
+ logTestInfo("expected url: " + url);
+ logTestInfo("returned url: " + gRequestURL);
+ }
+ Assert.equal(gRequestURL, url,
+ "the url" + MSG_SHOULD_EQUAL);
+ run_test_pt2();
+}
+
+// url constructed with:
+// %CHANNEL% with distribution partners
+// %CUSTOM% parameter
+// force param when there already is a param - bug 454357
+function run_test_pt2() {
+ gCheckFunc = check_test_pt2;
+ let url = URL_PREFIX + "%CHANNEL%/updates.xml?custom=%CUSTOM%";
+ debugDump("testing url constructed with %CHANNEL% - " + url);
+ setUpdateURL(url);
+ gDefaultPrefBranch.setCharPref(PREFBRANCH_APP_PARTNER + "test_partner1",
+ "test_partner1");
+ gDefaultPrefBranch.setCharPref(PREFBRANCH_APP_PARTNER + "test_partner2",
+ "test_partner2");
+ Services.prefs.setCharPref("app.update.custom", "custom");
+ gUpdateChecker.checkForUpdates(updateCheckListener, true);
+}
+
+function check_test_pt2() {
+ let url = URL_PREFIX + "test_channel-cck-test_partner1-test_partner2/" +
+ "updates.xml?custom=custom&force=1";
+ Assert.equal(gRequestURL, url,
+ "the url" + MSG_SHOULD_EQUAL);
+ doTestFinish();
+}
+
+function getABI() {
+ let abi;
+ try {
+ abi = gAppInfo.XPCOMABI;
+ } catch (e) {
+ do_throw("nsIXULAppInfo:XPCOMABI not defined\n");
+ }
+
+ if (IS_MACOSX) {
+ // Mac universal build should report a different ABI than either macppc
+ // or mactel. This is necessary since nsUpdateService.js will set the ABI to
+ // Universal-gcc3 for Mac universal builds.
+ let macutils = Cc["@mozilla.org/xpcom/mac-utils;1"].
+ getService(Ci.nsIMacUtils);
+
+ if (macutils.isUniversalBinary) {
+ abi += "-u-" + macutils.architecturesInBinary;
+ }
+ } else if (IS_WIN) {
+ // Windows build should report the CPU architecture that it's running on.
+ abi += "-" + getProcArchitecture();
+ }
+ return abi;
+}
+
+function getOSVersion() {
+ let sysInfo = Cc["@mozilla.org/system-info;1"].getService(Ci.nsIPropertyBag2);
+ let osVersion = sysInfo.getProperty("name") + " " + sysInfo.getProperty("version");
+
+ if (IS_WIN) {
+ try {
+ let servicePack = getServicePack();
+ osVersion += "." + servicePack;
+ } catch (e) {
+ do_throw("Failure obtaining service pack: " + e);
+ }
+
+ if ("5.0" === sysInfo.getProperty("version")) { // Win2K
+ osVersion += " (unknown)";
+ } else {
+ try {
+ osVersion += " (" + getProcArchitecture() + ")";
+ } catch (e) {
+ do_throw("Failed to obtain processor architecture: " + e);
+ }
+ }
+ }
+
+ if (osVersion) {
+ 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;
+}
+
+function getServicePack() {
+ // NOTE: This function is a helper function and not a test. Thus,
+ // it uses throw() instead of do_throw(). Any tests that use this function
+ // should catch exceptions thrown in this function and deal with them
+ // appropriately (usually by calling do_throw).
+ 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 = ctypes.open("kernel32");
+ try {
+ let GetVersionEx = kernel32.declare("GetVersionExW",
+ ctypes.default_abi,
+ BOOL,
+ OSVERSIONINFOEXW.ptr);
+ let winVer = OSVERSIONINFOEXW();
+ winVer.dwOSVersionInfoSize = OSVERSIONINFOEXW.size;
+
+ if (0 === GetVersionEx(winVer.address())) {
+ // Using "throw" instead of "do_throw" (see NOTE above)
+ throw ("Failure in GetVersionEx (returned 0)");
+ }
+
+ return winVer.wServicePackMajor + "." + winVer.wServicePackMinor;
+ } finally {
+ kernel32.close();
+ }
+}
+
+function getProcArchitecture() {
+ // NOTE: This function is a helper function and not a test. Thus,
+ // it uses throw() instead of do_throw(). Any tests that use this function
+ // should catch exceptions thrown in this function and deal with them
+ // appropriately (usually by calling do_throw).
+ const WORD = ctypes.uint16_t;
+ const DWORD = ctypes.uint32_t;
+
+ // 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 = ctypes.open("kernel32");
+ 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:
+ return "x64";
+ case 6:
+ return "IA64";
+ case 0:
+ return "x86";
+ default:
+ // Using "throw" instead of "do_throw" (see NOTE above)
+ throw ("Unknown architecture returned from GetNativeSystemInfo: " + sysInfo.wProcessorArchitecture);
+ }
+ } finally {
+ kernel32.close();
+ }
+}
+
+/**
+ * Provides system capability information for application update though it may
+ * be used by other consumers.
+ */
+function getSystemCapabilities() {
+ if (IS_WIN) {
+ const PF_MMX_INSTRUCTIONS_AVAILABLE = 3; // MMX
+ const PF_XMMI_INSTRUCTIONS_AVAILABLE = 6; // SSE
+ const PF_XMMI64_INSTRUCTIONS_AVAILABLE = 10; // SSE2
+ const PF_SSE3_INSTRUCTIONS_AVAILABLE = 13; // SSE3
+
+ let lib = ctypes.open("kernel32.dll");
+ let IsProcessorFeaturePresent = lib.declare("IsProcessorFeaturePresent",
+ ctypes.winapi_abi,
+ ctypes.int32_t, /* success */
+ ctypes.uint32_t); /* DWORD */
+ let instructionSet = "unknown";
+ try {
+ if (IsProcessorFeaturePresent(PF_SSE3_INSTRUCTIONS_AVAILABLE)) {
+ instructionSet = "SSE3";
+ } else if (IsProcessorFeaturePresent(PF_XMMI64_INSTRUCTIONS_AVAILABLE)) {
+ instructionSet = "SSE2";
+ } else if (IsProcessorFeaturePresent(PF_XMMI_INSTRUCTIONS_AVAILABLE)) {
+ instructionSet = "SSE";
+ } else if (IsProcessorFeaturePresent(PF_MMX_INSTRUCTIONS_AVAILABLE)) {
+ instructionSet = "MMX";
+ }
+ } catch (e) {
+ Cu.reportError("Error getting processor instruction set. " +
+ "Exception: " + e);
+ }
+
+ lib.close();
+ return instructionSet;
+ }
+
+ return "NA";
+}
diff --git a/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini
new file mode 100644
index 000000000..0d2205046
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_aus_update/xpcshell.ini
@@ -0,0 +1,27 @@
+; This Source Code Form is subject to the terms of the Mozilla Public
+; License, v. 2.0. If a copy of the MPL was not distributed with this
+; file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+[DEFAULT]
+tags = appupdate
+head = head_update.js
+tail =
+
+[canCheckForAndCanApplyUpdates.js]
+[urlConstruction.js]
+[updateManagerXML.js]
+[remoteUpdateXML.js]
+[downloadAndHashCheckMar.js]
+[cleanupDownloadingForOlderAppVersion.js]
+[cleanupDownloadingForDifferentChannel.js]
+[cleanupDownloadingForSameVersionAndBuildID.js]
+[cleanupDownloadingIncorrectStatus.js]
+[cleanupPendingVersionFileIncorrectStatus.js]
+[cleanupSuccessLogMove.js]
+[cleanupSuccessLogsFIFO.js]
+[downloadInterruptedRecovery.js]
+[downloadResumeForSameAppVersion.js]
+[downloadCompleteAfterPartialFailure.js]
+[uiSilentPref.js]
+[uiUnsupportedAlreadyNotified.js]
+[uiAutoPref.js]
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js b/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/head_update.js b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js
new file mode 100644
index 000000000..9715c5828
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/head_update.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const IS_SERVICE_TEST = false;
+
+/* import-globals-from ../data/xpcshellUtilsAUS.js */
+load("../data/xpcshellUtilsAUS.js");
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js
new file mode 100644
index 000000000..f3f767394
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFileNotInInstallDirFailure.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+/* Callback file not in install directory or a sub-directory of the install
+ directory failure */
+
+const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_DIR_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getTestDirFile(FILE_HELPER_BIN).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js
new file mode 100644
index 000000000..969f84f9d
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgCallbackFilePathTooLongFailure.js
@@ -0,0 +1,45 @@
+/* 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/.
+ */
+
+/* Too long callback file path failure test */
+
+const STATE_AFTER_RUNUPDATE = STATE_FAILED_INVALID_CALLBACK_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "123456789";
+ if (IS_WIN) {
+ path = "\\" + path;
+ path = path.repeat(30); // 300 characters
+ path = "C:" + path;
+ } else {
+ path = "/" + path;
+ path = path.repeat(1000); // 10000 characters
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, null, path);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js
new file mode 100644
index 000000000..70e03646a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTooLongFailure.js
@@ -0,0 +1,47 @@
+/* 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/.
+ */
+
+/* Too long install directory path failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "123456789";
+ if (IS_WIN) {
+ path = "\\" + path;
+ path = path.repeat(30); // 300 characters
+ path = "C:" + path;
+ } else {
+ path = "/" + path;
+ path = path.repeat(1000); // 10000 characters
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js
new file mode 100644
index 000000000..330578de6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallDirPathTraversalFailure.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Install directory path traversal failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "123456789";
+ if (IS_WIN) {
+ path = "C:\\" + path + "\\..\\" + path;
+ } else {
+ path = "/" + path + "/../" + path;
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js
new file mode 100644
index 000000000..8ddb34af0
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgInstallWorkingDirPathNotSameFailure_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Different install and working directories for a regular update failure */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR
+ : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getApplyDirFile("..", false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js
new file mode 100644
index 000000000..c8ae3f0c6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgPatchDirPathTraversalFailure.js
@@ -0,0 +1,43 @@
+/* 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/.
+ */
+
+/* Patch directory path traversal failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getUpdatesPatchDir();
+ if (IS_WIN) {
+ path = path + "\\..\\";
+ } else {
+ path = path + "/../";
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js
new file mode 100644
index 000000000..e9b227657
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgStageDirNotInInstallDirFailure_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Different install and working directories for a regular update failure */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR
+ : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getApplyDirFile("..", false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js
new file mode 100644
index 000000000..87bbad4aa
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathLocalUNCFailure_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Working directory path local UNC failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "\\\\.\\" + getApplyDirFile(null, false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js
new file mode 100644
index 000000000..a550909b2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/invalidArgWorkingDirPathRelativeFailure.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+/* Relative working directory path failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js
new file mode 100644
index 000000000..b9f793236
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyDirLockedStageFailure_win.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ createUpdateInProgressLockFile(getAppBaseDir());
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(false);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ removeUpdateInProgressLockFile(getAppBaseDir());
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(PERFORMING_STAGED_UPDATE);
+ checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js
new file mode 100644
index 000000000..a606720b7
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateAppBinInUseStageSuccess_win.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, undefined);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true, false);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ lockDirectory(getAppBaseDir().path);
+ // Switch the application to the staged application that was updated.
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js
new file mode 100644
index 000000000..00b38adc7
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageOldVersionFailure.js
@@ -0,0 +1,88 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test a replace request for a staged update with a version file that specifies
+ * an older version failure. The same check is used in nsUpdateDriver.cpp for
+ * all update types which is why there aren't tests for the maintenance service
+ * as well as for other update types.
+ */
+
+const STATE_AFTER_STAGE = STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(false);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Change the active update to an older version to simulate installing a new
+ // version of the application while there is an update that has been staged.
+ let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ let patches = getLocalPatchString(null, null, null, null, null, "true",
+ STATE_AFTER_STAGE);
+ let updates = getLocalUpdateString(patches, null, null, null, "1.0", null,
+ null, null, null, null, "true", channel);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ // Change the version file to an older version to simulate installing a new
+ // version of the application while there is an update that has been staged.
+ writeVersionFile("1.0");
+ reloadUpdateManagerData();
+ // Try to switch the application to the staged application that was updated.
+ runUpdateUsingApp(STATE_AFTER_STAGE);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_STAGE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile, IS_MACOSX ? false : true, false);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js
new file mode 100644
index 000000000..5b9b08156
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateStageSuccess.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js
new file mode 100644
index 000000000..e76233fe6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppApplyUpdateSuccess.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ // The third parameter will test that a full path to the post update binary
+ // doesn't execute.
+ setupUpdaterTest(FILE_COMPLETE_MAR, undefined,
+ getApplyDirFile(null, true).path + "/");
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js
new file mode 100644
index 000000000..b1505d58e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageFailureComplete_win.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Application in use complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js
new file mode 100644
index 000000000..a1cc7d043
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseStageSuccessComplete_unix.js
@@ -0,0 +1,113 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Application in use complete MAR file staged patch apply success test */
+
+const START_STATE = STATE_PENDING;
+const STATE_AFTER_STAGE = STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestFiles[gTestFiles.length - 1].originalContents = null;
+ gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n";
+ gTestFiles[gTestFiles.length - 1].comparePerms = 0o644;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ setupSymLinks();
+ runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ checkSymLinks();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkCallbackLog();
+}
+
+/**
+ * Setup symlinks for the test.
+ */
+function setupSymLinks() {
+ if (IS_UNIX) {
+ removeSymlink();
+ createSymlink();
+ do_register_cleanup(removeSymlink);
+ gTestFiles.splice(gTestFiles.length - 3, 0,
+ {
+ description: "Readable symlink",
+ fileName: "link",
+ relPathDir: DIR_RESOURCES,
+ originalContents: "test",
+ compareContents: "test",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ });
+ }
+}
+
+/**
+ * Checks the state of the symlinks for the test.
+ */
+function checkSymLinks() {
+ if (IS_UNIX) {
+ checkSymlink();
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js
new file mode 100644
index 000000000..93333cade
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marAppInUseSuccessComplete.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Application in use complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js
new file mode 100644
index 000000000..79e54c182
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessComplete_win.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Replace app binary complete MAR file staged patch apply success test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js
new file mode 100644
index 000000000..b1f84715f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppStageSuccessPartial_win.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Patch app binary partial MAR file staged patch apply success test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js
new file mode 100644
index 000000000..85e92d290
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessComplete_win.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Replace app binary complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js
new file mode 100644
index 000000000..1212c9ba2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marCallbackAppSuccessPartial_win.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Patch app binary partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js
new file mode 100644
index 000000000..960c96f7b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFailurePartial.js
@@ -0,0 +1,47 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Patch Apply Failure Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[11].originalFile = "partial.png";
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ // If execv is used the updater process will turn into the callback process
+ // and the updater's return code will be that of the callback process.
+ runUpdate(STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, false, (USE_EXECV ? 0 : 1),
+ true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_FAILURE);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js
new file mode 100644
index 000000000..b39595f92
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailureComplete_win.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js
new file mode 100644
index 000000000..06d386ad6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseStageFailurePartial_win.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use partial MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js
new file mode 100644
index 000000000..89a2fff5e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessComplete_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js
new file mode 100644
index 000000000..ea85ddccc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileInUseSuccessPartial_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js
new file mode 100644
index 000000000..c5efaa8c0
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailureComplete_win.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked complete MAR file patch apply failure test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[3]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_PENDING,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_BACKUP_CREATE_7);
+ checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js
new file mode 100644
index 000000000..4fdbadb5b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedFailurePartial_win.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked partial MAR file patch apply failure test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[2]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_FAILED_READ_ERROR, false, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_UNABLE_OPEN_DEST);
+ checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js
new file mode 100644
index 000000000..4d12f4e42
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailureComplete_win.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[3]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ // Files aren't checked after staging since this test locks a file which
+ // prevents reading the file.
+ checkUpdateLogContains(ERR_ENSURE_COPY);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_PENDING,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 2,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_BACKUP_CREATE_7);
+ checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js
new file mode 100644
index 000000000..5f64df34c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marFileLockedStageFailurePartial_win.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked partial MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[2]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ // Files aren't checked after staging since this test locks a file which
+ // prevents reading the file.
+ checkUpdateLogContains(ERR_ENSURE_COPY);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_FAILED_READ_ERROR, false, 1, false);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 2,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_UNABLE_OPEN_DEST);
+ checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js
new file mode 100644
index 000000000..b83bafccc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailureComplete_win.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir complete MAR file staged patch apply failure
+ test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] +
+ gTestDirs[4].subDirFiles[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js
new file mode 100644
index 000000000..39ea485cd
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseStageFailurePartial_win.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir partial MAR file staged patch apply failure
+ test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js
new file mode 100644
index 000000000..a71bb8d49
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessComplete_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] +
+ gTestDirs[4].subDirFiles[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js
new file mode 100644
index 000000000..2cbe70ed8
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marRMRFDirFileInUseSuccessPartial_win.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js
new file mode 100644
index 000000000..a9ce23420
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageFailurePartial.js
@@ -0,0 +1,41 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Staged Patch Apply Failure Test */
+
+const STATE_AFTER_STAGE = STATE_FAILED;
+gStagingRemovedUpdate = true;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[11].originalFile = "partial.png";
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js
new file mode 100644
index 000000000..f7745f68f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessComplete.js
@@ -0,0 +1,132 @@
+/* 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/.
+ */
+
+/* General Complete MAR File Staged Patch Apply Test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestFiles[gTestFiles.length - 1].originalContents = null;
+ gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n";
+ gTestFiles[gTestFiles.length - 1].comparePerms = 0o644;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupDistributionDir();
+ setupSymLinks();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ checkSymLinks();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are removed when there is a distribution
+ // directory in the new location.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test1/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory for the test.
+ */
+function checkDistributionDir() {
+ if (IS_MACOSX) {
+ let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ checkUpdateLogContains(REMOVE_OLD_DIST_DIR);
+ }
+}
+
+/**
+ * Setup symlinks for the test.
+ */
+function setupSymLinks() {
+ // Don't test symlinks on Mac OS X in this test since it tends to timeout.
+ // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js
+ if (IS_UNIX && !IS_MACOSX) {
+ removeSymlink();
+ createSymlink();
+ do_register_cleanup(removeSymlink);
+ gTestFiles.splice(gTestFiles.length - 3, 0,
+ {
+ description: "Readable symlink",
+ fileName: "link",
+ relPathDir: DIR_RESOURCES,
+ originalContents: "test",
+ compareContents: "test",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ });
+ }
+}
+
+/**
+ * Checks the state of the symlinks for the test.
+ */
+function checkSymLinks() {
+ // Don't test symlinks on Mac OS X in this test since it tends to timeout.
+ // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js
+ if (IS_UNIX && !IS_MACOSX) {
+ checkSymlink();
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js
new file mode 100644
index 000000000..ef15326de
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marStageSuccessPartial.js
@@ -0,0 +1,112 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Staged Patch Apply Test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[gTestFiles.length - 2].originalContents = null;
+ gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 2].comparePerms = 0o644;
+ gTestDirs = gTestDirsPartialSuccess;
+ preventDistributionFiles();
+ setupDistributionDir();
+ setupUpdaterTest(FILE_PARTIAL_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are moved to the new location on update.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true);
+ if (IS_MACOSX) {
+ Assert.ok(distributionDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(distributionDir.path));
+
+ let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+
+ checkUpdateLogContains(MOVE_OLD_DIST_DIR);
+ } else {
+ debugDump("testing that files aren't added with an add-if instruction " +
+ "when the file's destination directory doesn't exist");
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js
new file mode 100644
index 000000000..1008e867f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessComplete.js
@@ -0,0 +1,96 @@
+/* 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/.
+ */
+
+/* General Complete MAR File Patch Apply Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ preventDistributionFiles();
+ setupDistributionDir();
+ setupUpdaterTest(FILE_COMPLETE_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are moved to the new location on update.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true);
+ if (IS_MACOSX) {
+ Assert.ok(distributionDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(distributionDir.path));
+
+ let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+
+ checkUpdateLogContains(MOVE_OLD_DIST_DIR);
+ } else {
+ debugDump("testing that files aren't added with an add-if instruction " +
+ "when the file's destination directory doesn't exist");
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js
new file mode 100644
index 000000000..616390f55
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marSuccessPartial.js
@@ -0,0 +1,79 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Patch Apply Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[gTestFiles.length - 1].originalContents = null;
+ gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 1].comparePerms = 0o644;
+ gTestFiles[gTestFiles.length - 2].originalContents = null;
+ gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 2].comparePerms = 0o644;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupDistributionDir();
+ // The third parameter will test that a relative path that contains a
+ // directory traversal to the post update binary doesn't execute.
+ setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../");
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are removed when there is a distribution
+ // directory in the new location.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ if (IS_MACOSX) {
+ let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ checkUpdateLogContains(REMOVE_OLD_DIST_DIR);
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js
new file mode 100644
index 000000000..86a2eb821
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marVersionDowngrade.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Test version downgrade MAR security check */
+
+function run_test() {
+ if (!MOZ_VERIFY_MAR_SIGNATURE) {
+ return;
+ }
+
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_OLD_VERSION_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ // If execv is used the updater process will turn into the callback process
+ // and the updater's return code will be that of the callback process.
+ runUpdate(STATE_FAILED_VERSION_DOWNGRADE_ERROR, false, (USE_EXECV ? 0 : 1),
+ false);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, VERSION_DOWNGRADE_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(STATE_FAILED_VERSION_DOWNGRADE_ERROR);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js
new file mode 100644
index 000000000..6db906fbc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/marWrongChannel.js
@@ -0,0 +1,51 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Test product/channel MAR security check */
+
+function run_test() {
+ if (!MOZ_VERIFY_MAR_SIGNATURE) {
+ return;
+ }
+
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_WRONG_CHANNEL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ // If execv is used the updater process will turn into the callback process
+ // and the updater's return code will be that of the callback process.
+ runUpdate(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR, false, (USE_EXECV ? 0 : 1),
+ false);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, MAR_CHANNEL_MISMATCH_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
new file mode 100644
index 000000000..2b77bee7a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_base_updater/xpcshell.ini
@@ -0,0 +1,136 @@
+; 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/.
+
+; Tests that require the updater binary. These tests should never run on Android
+; which doesn't use the updater binary as other applications do and are excluded
+; from running the tests in the moz.build file.
+
+[DEFAULT]
+tags = appupdate
+head = head_update.js
+tail =
+
+[invalidArgCallbackFileNotInInstallDirFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[invalidArgCallbackFilePathTooLongFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[invalidArgInstallDirPathTooLongFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[invalidArgInstallDirPathTraversalFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[invalidArgInstallWorkingDirPathNotSameFailure_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[invalidArgPatchDirPathTraversalFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[invalidArgStageDirNotInInstallDirFailure_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[invalidArgWorkingDirPathLocalUNCFailure_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[invalidArgWorkingDirPathRelativeFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marSuccessComplete.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marSuccessPartial.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marFailurePartial.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marStageSuccessComplete.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marStageSuccessPartial.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marVersionDowngrade.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985 and mar signing
+[marWrongChannel.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985 and mar signing
+[marStageFailurePartial.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marCallbackAppSuccessComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marCallbackAppSuccessPartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marCallbackAppStageSuccessComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marCallbackAppStageSuccessPartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marAppInUseSuccessComplete.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marAppInUseStageSuccessComplete_unix.js]
+skip-if = os == 'win'
+reason = not a Windows test
+[marAppInUseStageFailureComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileLockedFailureComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileLockedFailurePartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileLockedStageFailureComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileLockedStageFailurePartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileInUseSuccessComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileInUseSuccessPartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marRMRFDirFileInUseSuccessComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marRMRFDirFileInUseSuccessPartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileInUseStageFailureComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marFileInUseStageFailurePartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marRMRFDirFileInUseStageFailureComplete_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marRMRFDirFileInUseStageFailurePartial_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marAppApplyDirLockedStageFailure_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marAppApplyUpdateAppBinInUseStageSuccess_win.js]
+skip-if = os != 'win' || debug && (os_version == '5.1' || os_version == '5.2')
+reason = Windows only test and bug 1291985
+[marAppApplyUpdateSuccess.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marAppApplyUpdateStageSuccess.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+[marAppApplyUpdateStageOldVersionFailure.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js b/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js
new file mode 100644
index 000000000..015fbd0cb
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/bootstrapSvc.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Bootstrap the tests using the service by installing our own version of the service */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ // We don't actually care if the MAR has any data, we only care about the
+ // application return code and update.status result.
+ gTestFiles = gTestFilesCommon;
+ gTestDirs = [];
+ setupUpdaterTest(FILE_COMPLETE_MAR, null);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdateUsingService finishes.
+ */
+function runUpdateFinished() {
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, false);
+
+ // We need to check the service log even though this is a bootstrap
+ // because the app bin could be in use by this test by the time the next
+ // test runs.
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js
new file mode 100644
index 000000000..bf765ee78
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/checkUpdaterSigSvc.js
@@ -0,0 +1,39 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * We skip authenticode cert checks from the service udpates
+ * so that we can use updater-xpcshell with the wrong certs for testing.
+ * This tests that code path. */
+
+function run_test() {
+ if (!IS_AUTHENTICODE_CHECK_ENABLED) {
+ return;
+ }
+
+ let binDir = getGREBinDir();
+ let maintenanceServiceBin = binDir.clone();
+ maintenanceServiceBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+
+ let updaterBin = binDir.clone();
+ updaterBin.append(FILE_UPDATER_BIN);
+
+ debugDump("Launching maintenance service bin: " +
+ maintenanceServiceBin.path + " to check updater: " +
+ updaterBin.path + " signature.");
+
+ // Bypass the manifest and run as invoker
+ gEnv.set("__COMPAT_LAYER", "RunAsInvoker");
+
+ let dummyInstallPath = "---";
+ let maintenanceServiceBinArgs = ["check-cert", dummyInstallPath,
+ updaterBin.path];
+ let maintenanceServiceBinProcess = Cc["@mozilla.org/process/util;1"].
+ createInstance(Ci.nsIProcess);
+ maintenanceServiceBinProcess.init(maintenanceServiceBin);
+ maintenanceServiceBinProcess.run(true, maintenanceServiceBinArgs,
+ maintenanceServiceBinArgs.length);
+ Assert.equal(maintenanceServiceBinProcess.exitValue, 0,
+ "the maintenance service exit value should be 0");
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/head_update.js b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js
new file mode 100644
index 000000000..38be4ee39
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/head_update.js
@@ -0,0 +1,8 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+const IS_SERVICE_TEST = true;
+
+/* import-globals-from ../data/xpcshellUtilsAUS.js */
+load("../data/xpcshellUtilsAUS.js");
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js
new file mode 100644
index 000000000..70e03646a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTooLongFailureSvc.js
@@ -0,0 +1,47 @@
+/* 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/.
+ */
+
+/* Too long install directory path failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "123456789";
+ if (IS_WIN) {
+ path = "\\" + path;
+ path = path.repeat(30); // 300 characters
+ path = "C:" + path;
+ } else {
+ path = "/" + path;
+ path = path.repeat(1000); // 10000 characters
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js
new file mode 100644
index 000000000..330578de6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallDirPathTraversalFailureSvc.js
@@ -0,0 +1,44 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Install directory path traversal failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "123456789";
+ if (IS_WIN) {
+ path = "C:\\" + path + "\\..\\" + path;
+ } else {
+ path = "/" + path + "/../" + path;
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, path, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js
new file mode 100644
index 000000000..8ddb34af0
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Different install and working directories for a regular update failure */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR
+ : STATE_FAILED_INVALID_APPLYTO_DIR_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getApplyDirFile("..", false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js
new file mode 100644
index 000000000..c8ae3f0c6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgPatchDirPathTraversalFailureSvc.js
@@ -0,0 +1,43 @@
+/* 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/.
+ */
+
+/* Patch directory path traversal failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getUpdatesPatchDir();
+ if (IS_WIN) {
+ path = path + "\\..\\";
+ } else {
+ path = path + "/../";
+ }
+
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, path, null, null, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js
new file mode 100644
index 000000000..e9b227657
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgStageDirNotInInstallDirFailureSvc_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Different install and working directories for a regular update failure */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR
+ : STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = getApplyDirFile("..", false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js
new file mode 100644
index 000000000..87bbad4aa
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathLocalUNCFailureSvc_win.js
@@ -0,0 +1,38 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/.
+ */
+
+/* Working directory path local UNC failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ let path = "\\\\.\\" + getApplyDirFile(null, false).path;
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, path, null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js
new file mode 100644
index 000000000..a550909b2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/invalidArgWorkingDirPathRelativeFailureSvc.js
@@ -0,0 +1,37 @@
+/* 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/.
+ */
+
+/* Relative working directory path failure test */
+
+const STATE_AFTER_RUNUPDATE =
+ IS_SERVICE_TEST ? STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR
+ : STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_AFTER_RUNUPDATE, false, 1, true, null, null, "test", null);
+}
+
+/**
+ * Called after the call to runUpdateUsingUpdater finishes.
+ */
+function runUpdateFinished() {
+ standardInit();
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js
new file mode 100644
index 000000000..b9f793236
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyDirLockedStageFailureSvc_win.js
@@ -0,0 +1,41 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ createUpdateInProgressLockFile(getAppBaseDir());
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(false);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ removeUpdateInProgressLockFile(getAppBaseDir());
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(PERFORMING_STAGED_UPDATE);
+ checkUpdateLogContains(ERR_UPDATE_IN_PROGRESS);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js
new file mode 100644
index 000000000..a606720b7
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js
@@ -0,0 +1,83 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, undefined);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true, false);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ lockDirectory(getAppBaseDir().path);
+ // Switch the application to the staged application that was updated.
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js
new file mode 100644
index 000000000..5b9b08156
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateStageSuccessSvc.js
@@ -0,0 +1,82 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js
new file mode 100644
index 000000000..e76233fe6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppApplyUpdateSuccessSvc.js
@@ -0,0 +1,65 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Test applying an update by staging an update and launching an application to
+ * apply it.
+ */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ // The third parameter will test that a full path to the post update binary
+ // doesn't execute.
+ setupUpdaterTest(FILE_COMPLETE_MAR, undefined,
+ getApplyDirFile(null, true).path + "/");
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdateUsingApp(STATE_SUCCEEDED);
+}
+
+/**
+ * Called after the call to runUpdateUsingApp finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+
+ let updatesDir = getUpdatesPatchDir();
+ Assert.ok(updatesDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(updatesDir.path));
+
+ let log = getUpdateLog(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateLog(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js
new file mode 100644
index 000000000..b1505d58e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseStageFailureCompleteSvc_win.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Application in use complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js
new file mode 100644
index 000000000..93333cade
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marAppInUseSuccessCompleteSvc.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Application in use complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(DIR_RESOURCES + gCallbackBinFile, false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js
new file mode 100644
index 000000000..79e54c182
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessCompleteSvc_win.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Replace app binary complete MAR file staged patch apply success test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js
new file mode 100644
index 000000000..b1f84715f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppStageSuccessPartialSvc_win.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Patch app binary partial MAR file staged patch apply success test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js
new file mode 100644
index 000000000..85e92d290
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessCompleteSvc_win.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Replace app binary complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js
new file mode 100644
index 000000000..1212c9ba2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marCallbackAppSuccessPartialSvc_win.js
@@ -0,0 +1,48 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* Patch app binary partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ gCallbackBinFile = "exe0.exe";
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js
new file mode 100644
index 000000000..960c96f7b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFailurePartialSvc.js
@@ -0,0 +1,47 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Patch Apply Failure Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[11].originalFile = "partial.png";
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ // If execv is used the updater process will turn into the callback process
+ // and the updater's return code will be that of the callback process.
+ runUpdate(STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE, false, (USE_EXECV ? 0 : 1),
+ true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_FAILURE);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js
new file mode 100644
index 000000000..b39595f92
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailureCompleteSvc_win.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js
new file mode 100644
index 000000000..06d386ad6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseStageFailurePartialSvc_win.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use partial MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js
new file mode 100644
index 000000000..89a2fff5e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessCompleteSvc_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[13].relPathDir + gTestFiles[13].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js
new file mode 100644
index 000000000..ea85ddccc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileInUseSuccessPartialSvc_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestFiles[11].relPathDir + gTestFiles[11].fileName,
+ false);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js
new file mode 100644
index 000000000..c5efaa8c0
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailureCompleteSvc_win.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked complete MAR file patch apply failure test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[3]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_PENDING,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_BACKUP_CREATE_7);
+ checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js
new file mode 100644
index 000000000..4fdbadb5b
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedFailurePartialSvc_win.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked partial MAR file patch apply failure test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[2]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_FAILED_READ_ERROR, false, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_UNABLE_OPEN_DEST);
+ checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js
new file mode 100644
index 000000000..4d12f4e42
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailureCompleteSvc_win.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked complete MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[3]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ // Files aren't checked after staging since this test locks a file which
+ // prevents reading the file.
+ checkUpdateLogContains(ERR_ENSURE_COPY);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_FAILED_WRITE_ERROR, false, 1, false);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_PENDING,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 2,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_PENDING,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, WRITE_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_BACKUP_CREATE_7);
+ checkUpdateLogContains(STATE_FAILED_WRITE_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js
new file mode 100644
index 000000000..5f64df34c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marFileLockedStageFailurePartialSvc_win.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File locked partial MAR file staged patch apply failure test */
+
+const STATE_AFTER_STAGE = STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperLockFile(gTestFiles[2]);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ // Files aren't checked after staging since this test locks a file which
+ // prevents reading the file.
+ checkUpdateLogContains(ERR_ENSURE_COPY);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_FAILED_READ_ERROR, false, 1, false);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusFile(), STATE_NONE,
+ "the status file failure code" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.updateCount, 2,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_FAILED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, READ_ERROR,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_UNABLE_OPEN_DEST);
+ checkUpdateLogContains(STATE_FAILED_READ_ERROR + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js
new file mode 100644
index 000000000..b83bafccc
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailureCompleteSvc_win.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir complete MAR file staged patch apply failure
+ test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] +
+ gTestDirs[4].subDirFiles[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js
new file mode 100644
index 000000000..39ea485cd
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseStageFailurePartialSvc_win.js
@@ -0,0 +1,71 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir partial MAR file staged patch apply failure
+ test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+const STATE_AFTER_RUNUPDATE = IS_SERVICE_TEST ? STATE_PENDING_SVC : STATE_PENDING;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_AFTER_RUNUPDATE, true, 1, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_AFTER_RUNUPDATE,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ setTestFilesAndDirsForFailure();
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_RENAME_FILE);
+ checkUpdateLogContains(ERR_MOVE_DESTDIR_7 + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js
new file mode 100644
index 000000000..a71bb8d49
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessCompleteSvc_win.js
@@ -0,0 +1,63 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir complete MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[4].relPathDir + gTestDirs[4].subDirs[0] +
+ gTestDirs[4].subDirFiles[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js
new file mode 100644
index 000000000..2cbe70ed8
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marRMRFDirFileInUseSuccessPartialSvc_win.js
@@ -0,0 +1,62 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/* File in use inside removed dir partial MAR file patch apply success test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runHelperFileInUse(gTestDirs[2].relPathDir + gTestDirs[2].files[0], true);
+}
+
+/**
+ * Called after the call to waitForHelperSleep finishes.
+ */
+function waitForHelperSleepFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ waitForHelperExit();
+}
+
+/**
+ * Called after the call to waitForHelperExit finishes.
+ */
+function waitForHelperExitFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContains(ERR_BACKUP_DISCARD);
+ checkUpdateLogContains(STATE_SUCCEEDED + "\n" + CALL_QUIT);
+ checkCallbackLog();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js
new file mode 100644
index 000000000..a9ce23420
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageFailurePartialSvc.js
@@ -0,0 +1,41 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Staged Patch Apply Failure Test */
+
+const STATE_AFTER_STAGE = STATE_FAILED;
+gStagingRemovedUpdate = true;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[11].originalFile = "partial.png";
+ gTestDirs = gTestDirsPartialSuccess;
+ setTestFilesAndDirsForFailure();
+ setupUpdaterTest(FILE_PARTIAL_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).errorCode, LOADSOURCE_ERROR_WRONG_SIZE,
+ "the update errorCode" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateFailure(getApplyDirFile);
+ checkUpdateLogContains(ERR_LOADSOURCEFILE_FAILED);
+ waitForFilesInUse();
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js
new file mode 100644
index 000000000..f7745f68f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessCompleteSvc.js
@@ -0,0 +1,132 @@
+/* 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/.
+ */
+
+/* General Complete MAR File Staged Patch Apply Test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestFiles[gTestFiles.length - 1].originalContents = null;
+ gTestFiles[gTestFiles.length - 1].compareContents = "FromComplete\n";
+ gTestFiles[gTestFiles.length - 1].comparePerms = 0o644;
+ gTestDirs = gTestDirsCompleteSuccess;
+ setupDistributionDir();
+ setupSymLinks();
+ setupUpdaterTest(FILE_COMPLETE_MAR, false);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ checkSymLinks();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are removed when there is a distribution
+ // directory in the new location.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test1/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory for the test.
+ */
+function checkDistributionDir() {
+ if (IS_MACOSX) {
+ let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ checkUpdateLogContains(REMOVE_OLD_DIST_DIR);
+ }
+}
+
+/**
+ * Setup symlinks for the test.
+ */
+function setupSymLinks() {
+ // Don't test symlinks on Mac OS X in this test since it tends to timeout.
+ // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js
+ if (IS_UNIX && !IS_MACOSX) {
+ removeSymlink();
+ createSymlink();
+ do_register_cleanup(removeSymlink);
+ gTestFiles.splice(gTestFiles.length - 3, 0,
+ {
+ description: "Readable symlink",
+ fileName: "link",
+ relPathDir: DIR_RESOURCES,
+ originalContents: "test",
+ compareContents: "test",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o666,
+ comparePerms: 0o666
+ });
+ }
+}
+
+/**
+ * Checks the state of the symlinks for the test.
+ */
+function checkSymLinks() {
+ // Don't test symlinks on Mac OS X in this test since it tends to timeout.
+ // It is tested on Mac OS X in marAppInUseStageSuccessComplete_unix.js
+ if (IS_UNIX && !IS_MACOSX) {
+ checkSymlink();
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js
new file mode 100644
index 000000000..ef15326de
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marStageSuccessPartialSvc.js
@@ -0,0 +1,112 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Staged Patch Apply Test */
+
+const STATE_AFTER_STAGE = IS_SERVICE_TEST ? STATE_APPLIED_SVC : STATE_APPLIED;
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[gTestFiles.length - 2].originalContents = null;
+ gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 2].comparePerms = 0o644;
+ gTestDirs = gTestDirsPartialSuccess;
+ preventDistributionFiles();
+ setupDistributionDir();
+ setupUpdaterTest(FILE_PARTIAL_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ stageUpdate(true);
+}
+
+/**
+ * Called after the call to stageUpdate finishes.
+ */
+function stageUpdateFinished() {
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getStageDirFile, true);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS, true, false, true);
+ // Switch the application to the staged application that was updated.
+ runUpdate(STATE_SUCCEEDED, true, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile, false, true);
+ checkUpdateLogContents(LOG_REPLACE_SUCCESS, false, true, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are moved to the new location on update.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true);
+ if (IS_MACOSX) {
+ Assert.ok(distributionDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(distributionDir.path));
+
+ let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+
+ checkUpdateLogContains(MOVE_OLD_DIST_DIR);
+ } else {
+ debugDump("testing that files aren't added with an add-if instruction " +
+ "when the file's destination directory doesn't exist");
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js
new file mode 100644
index 000000000..1008e867f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessCompleteSvc.js
@@ -0,0 +1,96 @@
+/* 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/.
+ */
+
+/* General Complete MAR File Patch Apply Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesCompleteSuccess;
+ gTestDirs = gTestDirsCompleteSuccess;
+ preventDistributionFiles();
+ setupDistributionDir();
+ setupUpdaterTest(FILE_COMPLETE_MAR, true);
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkPostUpdateAppLog();
+}
+
+/**
+ * Called after the call to checkPostUpdateAppLog finishes.
+ */
+function checkPostUpdateAppLogFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(true);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_COMPLETE_SUCCESS, false, false, true);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are moved to the new location on update.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ let distributionDir = getApplyDirFile(DIR_RESOURCES + "distribution", true);
+ if (IS_MACOSX) {
+ Assert.ok(distributionDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(distributionDir.path));
+
+ let testFile = getApplyDirFile(DIR_RESOURCES + "distribution/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ testFile = getApplyDirFile(DIR_RESOURCES + "distribution/test/testFile", true);
+ Assert.ok(testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path));
+
+ distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+
+ checkUpdateLogContains(MOVE_OLD_DIST_DIR);
+ } else {
+ debugDump("testing that files aren't added with an add-if instruction " +
+ "when the file's destination directory doesn't exist");
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js
new file mode 100644
index 000000000..616390f55
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/marSuccessPartialSvc.js
@@ -0,0 +1,79 @@
+/* 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/.
+ */
+
+/* General Partial MAR File Patch Apply Test */
+
+function run_test() {
+ if (!setupTestCommon()) {
+ return;
+ }
+ gTestFiles = gTestFilesPartialSuccess;
+ gTestFiles[gTestFiles.length - 1].originalContents = null;
+ gTestFiles[gTestFiles.length - 1].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 1].comparePerms = 0o644;
+ gTestFiles[gTestFiles.length - 2].originalContents = null;
+ gTestFiles[gTestFiles.length - 2].compareContents = "FromPartial\n";
+ gTestFiles[gTestFiles.length - 2].comparePerms = 0o644;
+ gTestDirs = gTestDirsPartialSuccess;
+ setupDistributionDir();
+ // The third parameter will test that a relative path that contains a
+ // directory traversal to the post update binary doesn't execute.
+ setupUpdaterTest(FILE_PARTIAL_MAR, false, "test/../");
+}
+
+/**
+ * Called after the call to setupUpdaterTest finishes.
+ */
+function setupUpdaterTestFinished() {
+ runUpdate(STATE_SUCCEEDED, false, 0, true);
+}
+
+/**
+ * Called after the call to runUpdate finishes.
+ */
+function runUpdateFinished() {
+ checkAppBundleModTime();
+ standardInit();
+ Assert.equal(readStatusState(), STATE_NONE,
+ "the status file state" + MSG_SHOULD_EQUAL);
+ Assert.ok(!gUpdateManager.activeUpdate,
+ "the active update should not be defined");
+ Assert.equal(gUpdateManager.updateCount, 1,
+ "the update manager updateCount attribute" + MSG_SHOULD_EQUAL);
+ Assert.equal(gUpdateManager.getUpdateAt(0).state, STATE_SUCCEEDED,
+ "the update state" + MSG_SHOULD_EQUAL);
+ checkPostUpdateRunningFile(false);
+ checkFilesAfterUpdateSuccess(getApplyDirFile);
+ checkUpdateLogContents(LOG_PARTIAL_SUCCESS);
+ checkDistributionDir();
+ checkCallbackLog();
+}
+
+/**
+ * Setup the state of the distribution directory for the test.
+ */
+function setupDistributionDir() {
+ if (IS_MACOSX) {
+ // Create files in the old distribution directory location to verify that
+ // the directory and its contents are removed when there is a distribution
+ // directory in the new location.
+ let testFile = getApplyDirFile(DIR_MACOS + "distribution/testFile", true);
+ writeFile(testFile, "test\n");
+ testFile = getApplyDirFile(DIR_MACOS + "distribution/test/testFile", true);
+ writeFile(testFile, "test\n");
+ }
+}
+
+/**
+ * Checks the state of the distribution directory.
+ */
+function checkDistributionDir() {
+ if (IS_MACOSX) {
+ let distributionDir = getApplyDirFile(DIR_MACOS + "distribution", true);
+ Assert.ok(!distributionDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(distributionDir.path));
+ checkUpdateLogContains(REMOVE_OLD_DIST_DIR);
+ }
+}
diff --git a/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini
new file mode 100644
index 000000000..1d63f8583
--- /dev/null
+++ b/toolkit/mozapps/update/tests/unit_service_updater/xpcshell.ini
@@ -0,0 +1,156 @@
+; 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/.
+
+; Tests that require the updater binary and the maintenance service.
+
+[DEFAULT]
+tags = appupdate
+head = head_update.js
+tail =
+
+[bootstrapSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgInstallDirPathTooLongFailureSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgInstallDirPathTraversalFailureSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgInstallWorkingDirPathNotSameFailureSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgPatchDirPathTraversalFailureSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgStageDirNotInInstallDirFailureSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgWorkingDirPathLocalUNCFailureSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[invalidArgWorkingDirPathRelativeFailureSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marSuccessCompleteSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marSuccessPartialSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFailurePartialSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marStageSuccessCompleteSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marStageSuccessPartialSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marStageFailurePartialSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marCallbackAppSuccessCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marCallbackAppSuccessPartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marCallbackAppStageSuccessCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marCallbackAppStageSuccessPartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppInUseSuccessCompleteSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppInUseStageFailureCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileLockedFailureCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileLockedFailurePartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileLockedStageFailureCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileLockedStageFailurePartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileInUseSuccessCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileInUseSuccessPartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marRMRFDirFileInUseSuccessCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marRMRFDirFileInUseSuccessPartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileInUseStageFailureCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marFileInUseStageFailurePartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marRMRFDirFileInUseStageFailureCompleteSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marRMRFDirFileInUseStageFailurePartialSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppApplyDirLockedStageFailureSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppApplyUpdateAppBinInUseStageSuccessSvc_win.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppApplyUpdateSuccessSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[marAppApplyUpdateStageSuccessSvc.js]
+skip-if = os == 'win' && debug && (os_version == '5.1' || os_version == '5.2')
+reason = bug 1291985
+run-sequentially = Uses the Mozilla Maintenance Service.
+[checkUpdaterSigSvc.js]
diff --git a/toolkit/mozapps/update/updater/Launchd.plist b/toolkit/mozapps/update/updater/Launchd.plist
new file mode 100644
index 000000000..f0b5cef08
--- /dev/null
+++ b/toolkit/mozapps/update/updater/Launchd.plist
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>Label</key>
+ <string>org.mozilla.updater</string>
+ <key>RunAtLoad</key>
+ <true/>
+</dict>
+</plist>
diff --git a/toolkit/mozapps/update/updater/Makefile.in b/toolkit/mozapps/update/updater/Makefile.in
new file mode 100644
index 000000000..84a843d18
--- /dev/null
+++ b/toolkit/mozapps/update/updater/Makefile.in
@@ -0,0 +1,29 @@
+# vim:set ts=8 sw=8 sts=8 noet:
+# 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/.
+
+# For changes here, also consider ./updater-xpcshell/Makefile.in
+
+ifndef MOZ_WINCONSOLE
+ifdef MOZ_DEBUG
+MOZ_WINCONSOLE = 1
+else
+MOZ_WINCONSOLE = 0
+endif
+endif
+
+include $(topsrcdir)/config/rules.mk
+
+ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+export::
+ sed -e 's/%MOZ_MACBUNDLE_ID%/$(MOZ_MACBUNDLE_ID)/' $(srcdir)/macbuild/Contents/Info.plist.in > $(DIST)/bin/Info.plist
+libs::
+ $(NSINSTALL) -D $(DIST)/bin/updater.app
+ rsync -a -C --exclude '*.in' $(srcdir)/macbuild/Contents $(DIST)/bin/updater.app
+ rsync -a -C $(DIST)/bin/Info.plist $(DIST)/bin/updater.app/Contents
+ sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \
+ iconv -f UTF-8 -t UTF-16 > $(DIST)/bin/updater.app/Contents/Resources/English.lproj/InfoPlist.strings
+ $(NSINSTALL) -D $(DIST)/bin/updater.app/Contents/MacOS
+ $(NSINSTALL) $(DIST)/bin/org.mozilla.updater $(DIST)/bin/updater.app/Contents/MacOS
+endif
diff --git a/toolkit/mozapps/update/updater/archivereader.cpp b/toolkit/mozapps/update/updater/archivereader.cpp
new file mode 100644
index 000000000..90cf45c3d
--- /dev/null
+++ b/toolkit/mozapps/update/updater/archivereader.cpp
@@ -0,0 +1,324 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <string.h>
+#include <stdlib.h>
+#include <fcntl.h>
+#include "bzlib.h"
+#include "archivereader.h"
+#include "errors.h"
+#ifdef XP_WIN
+#include "nsAlgorithm.h" // Needed by nsVersionComparator.cpp
+#include "updatehelper.h"
+#endif
+
+// These are generated at compile time based on the DER file for the channel
+// being used
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+#ifdef TEST_UPDATER
+#include "../xpcshellCert.h"
+#else
+#include "primaryCert.h"
+#include "secondaryCert.h"
+#endif
+#endif
+
+#define UPDATER_NO_STRING_GLUE_STL
+#include "nsVersionComparator.cpp"
+#undef UPDATER_NO_STRING_GLUE_STL
+
+#if defined(XP_UNIX)
+# include <sys/types.h>
+#elif defined(XP_WIN)
+# include <io.h>
+#endif
+
+static int inbuf_size = 262144;
+static int outbuf_size = 262144;
+static char *inbuf = nullptr;
+static char *outbuf = nullptr;
+
+/**
+ * Performs a verification on the opened MAR file with the passed in
+ * certificate name ID and type ID.
+ *
+ * @param archive The MAR file to verify the signature on.
+ * @param certData The certificate data.
+ * @return OK on success, CERT_VERIFY_ERROR on failure.
+*/
+template<uint32_t SIZE>
+int
+VerifyLoadedCert(MarFile *archive, const uint8_t (&certData)[SIZE])
+{
+ (void)archive;
+ (void)certData;
+
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+ const uint32_t size = SIZE;
+ const uint8_t* const data = &certData[0];
+ if (mar_verify_signatures(archive, &data, &size, 1)) {
+ return CERT_VERIFY_ERROR;
+ }
+#endif
+
+ return OK;
+}
+
+/**
+ * Performs a verification on the opened MAR file. Both the primary and backup
+ * keys stored are stored in the current process and at least the primary key
+ * will be tried. Success will be returned as long as one of the two
+ * signatures verify.
+ *
+ * @return OK on success
+*/
+int
+ArchiveReader::VerifySignature()
+{
+ if (!mArchive) {
+ return ARCHIVE_NOT_OPEN;
+ }
+
+#ifndef MOZ_VERIFY_MAR_SIGNATURE
+ return OK;
+#else
+#ifdef TEST_UPDATER
+ int rv = VerifyLoadedCert(mArchive, xpcshellCertData);
+#else
+ int rv = VerifyLoadedCert(mArchive, primaryCertData);
+ if (rv != OK) {
+ rv = VerifyLoadedCert(mArchive, secondaryCertData);
+ }
+#endif
+ return rv;
+#endif
+}
+
+/**
+ * Verifies that the MAR file matches the current product, channel, and version
+ *
+ * @param MARChannelID The MAR channel name to use, only updates from MARs
+ * with a matching MAR channel name will succeed.
+ * If an empty string is passed, no check will be done
+ * for the channel name in the product information block.
+ * If a comma separated list of values is passed then
+ * one value must match.
+ * @param appVersion The application version to use, only MARs with an
+ * application version >= to appVersion will be applied.
+ * @return OK on success
+ * COULD_NOT_READ_PRODUCT_INFO_BLOCK if the product info block
+ * could not be read.
+ * MARCHANNEL_MISMATCH_ERROR if update-settings.ini's MAR
+ * channel ID doesn't match the MAR
+ * file's MAR channel ID.
+ * VERSION_DOWNGRADE_ERROR if the application version for
+ * this updater is newer than the
+ * one in the MAR.
+ */
+int
+ArchiveReader::VerifyProductInformation(const char *MARChannelID,
+ const char *appVersion)
+{
+ if (!mArchive) {
+ return ARCHIVE_NOT_OPEN;
+ }
+
+ ProductInformationBlock productInfoBlock;
+ int rv = mar_read_product_info_block(mArchive,
+ &productInfoBlock);
+ if (rv != OK) {
+ return COULD_NOT_READ_PRODUCT_INFO_BLOCK_ERROR;
+ }
+
+ // Only check the MAR channel name if specified, it should be passed in from
+ // the update-settings.ini file.
+ if (MARChannelID && strlen(MARChannelID)) {
+ // Check for at least one match in the comma separated list of values.
+ const char *delimiter = " ,\t";
+ // Make a copy of the string in case a read only memory buffer
+ // was specified. strtok modifies the input buffer.
+ char channelCopy[512] = { 0 };
+ strncpy(channelCopy, MARChannelID, sizeof(channelCopy) - 1);
+ char *channel = strtok(channelCopy, delimiter);
+ rv = MAR_CHANNEL_MISMATCH_ERROR;
+ while(channel) {
+ if (!strcmp(channel, productInfoBlock.MARChannelID)) {
+ rv = OK;
+ break;
+ }
+ channel = strtok(nullptr, delimiter);
+ }
+ }
+
+ if (rv == OK) {
+ /* Compare both versions to ensure we don't have a downgrade
+ -1 if appVersion is older than productInfoBlock.productVersion
+ 1 if appVersion is newer than productInfoBlock.productVersion
+ 0 if appVersion is the same as productInfoBlock.productVersion
+ This even works with strings like:
+ - 12.0a1 being older than 12.0a2
+ - 12.0a2 being older than 12.0b1
+ - 12.0a1 being older than 12.0
+ - 12.0 being older than 12.1a1 */
+ int versionCompareResult =
+ mozilla::CompareVersions(appVersion, productInfoBlock.productVersion);
+ if (1 == versionCompareResult) {
+ rv = VERSION_DOWNGRADE_ERROR;
+ }
+ }
+
+ free((void *)productInfoBlock.MARChannelID);
+ free((void *)productInfoBlock.productVersion);
+ return rv;
+}
+
+int
+ArchiveReader::Open(const NS_tchar *path)
+{
+ if (mArchive)
+ Close();
+
+ if (!inbuf) {
+ inbuf = (char *)malloc(inbuf_size);
+ if (!inbuf) {
+ // Try again with a smaller buffer.
+ inbuf_size = 1024;
+ inbuf = (char *)malloc(inbuf_size);
+ if (!inbuf)
+ return ARCHIVE_READER_MEM_ERROR;
+ }
+ }
+
+ if (!outbuf) {
+ outbuf = (char *)malloc(outbuf_size);
+ if (!outbuf) {
+ // Try again with a smaller buffer.
+ outbuf_size = 1024;
+ outbuf = (char *)malloc(outbuf_size);
+ if (!outbuf)
+ return ARCHIVE_READER_MEM_ERROR;
+ }
+ }
+
+#ifdef XP_WIN
+ mArchive = mar_wopen(path);
+#else
+ mArchive = mar_open(path);
+#endif
+ if (!mArchive)
+ return READ_ERROR;
+
+ return OK;
+}
+
+void
+ArchiveReader::Close()
+{
+ if (mArchive) {
+ mar_close(mArchive);
+ mArchive = nullptr;
+ }
+
+ if (inbuf) {
+ free(inbuf);
+ inbuf = nullptr;
+ }
+
+ if (outbuf) {
+ free(outbuf);
+ outbuf = nullptr;
+ }
+}
+
+int
+ArchiveReader::ExtractFile(const char *name, const NS_tchar *dest)
+{
+ const MarItem *item = mar_find_item(mArchive, name);
+ if (!item)
+ return READ_ERROR;
+
+#ifdef XP_WIN
+ FILE* fp = _wfopen(dest, L"wb+");
+#else
+ int fd = creat(dest, item->flags);
+ if (fd == -1)
+ return WRITE_ERROR;
+
+ FILE *fp = fdopen(fd, "wb");
+#endif
+ if (!fp)
+ return WRITE_ERROR;
+
+ int rv = ExtractItemToStream(item, fp);
+
+ fclose(fp);
+ return rv;
+}
+
+int
+ArchiveReader::ExtractFileToStream(const char *name, FILE *fp)
+{
+ const MarItem *item = mar_find_item(mArchive, name);
+ if (!item)
+ return READ_ERROR;
+
+ return ExtractItemToStream(item, fp);
+}
+
+int
+ArchiveReader::ExtractItemToStream(const MarItem *item, FILE *fp)
+{
+ /* decompress the data chunk by chunk */
+
+ bz_stream strm;
+ int offset, inlen, outlen, ret = OK;
+
+ memset(&strm, 0, sizeof(strm));
+ if (BZ2_bzDecompressInit(&strm, 0, 0) != BZ_OK)
+ return UNEXPECTED_BZIP_ERROR;
+
+ offset = 0;
+ for (;;) {
+ if (!item->length) {
+ ret = UNEXPECTED_MAR_ERROR;
+ break;
+ }
+
+ if (offset < (int) item->length && strm.avail_in == 0) {
+ inlen = mar_read(mArchive, item, offset, inbuf, inbuf_size);
+ if (inlen <= 0)
+ return READ_ERROR;
+ offset += inlen;
+ strm.next_in = inbuf;
+ strm.avail_in = inlen;
+ }
+
+ strm.next_out = outbuf;
+ strm.avail_out = outbuf_size;
+
+ ret = BZ2_bzDecompress(&strm);
+ if (ret != BZ_OK && ret != BZ_STREAM_END) {
+ ret = UNEXPECTED_BZIP_ERROR;
+ break;
+ }
+
+ outlen = outbuf_size - strm.avail_out;
+ if (outlen) {
+ if (fwrite(outbuf, outlen, 1, fp) != 1) {
+ ret = WRITE_ERROR_EXTRACT;
+ break;
+ }
+ }
+
+ if (ret == BZ_STREAM_END) {
+ ret = OK;
+ break;
+ }
+ }
+
+ BZ2_bzDecompressEnd(&strm);
+ return ret;
+}
diff --git a/toolkit/mozapps/update/updater/archivereader.h b/toolkit/mozapps/update/updater/archivereader.h
new file mode 100644
index 000000000..a9d78aab1
--- /dev/null
+++ b/toolkit/mozapps/update/updater/archivereader.h
@@ -0,0 +1,41 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 ArchiveReader_h__
+#define ArchiveReader_h__
+
+#include <stdio.h>
+#include "mar.h"
+
+#ifdef XP_WIN
+ typedef WCHAR NS_tchar;
+#else
+ typedef char NS_tchar;
+#endif
+
+// This class provides an API to extract files from an update archive.
+class ArchiveReader
+{
+public:
+ ArchiveReader() : mArchive(nullptr) {}
+ ~ArchiveReader() { Close(); }
+
+ int Open(const NS_tchar *path);
+ int VerifySignature();
+ int VerifyProductInformation(const char *MARChannelID,
+ const char *appVersion);
+ void Close();
+
+ int ExtractFile(const char *item, const NS_tchar *destination);
+ int ExtractFileToStream(const char *item, FILE *fp);
+
+private:
+ int ExtractItemToStream(const MarItem *item, FILE *fp);
+
+ MarFile *mArchive;
+};
+
+#endif // ArchiveReader_h__
diff --git a/toolkit/mozapps/update/updater/automounter_gonk.cpp b/toolkit/mozapps/update/updater/automounter_gonk.cpp
new file mode 100644
index 000000000..3dff2a133
--- /dev/null
+++ b/toolkit/mozapps/update/updater/automounter_gonk.cpp
@@ -0,0 +1,251 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <android/log.h>
+#include <cutils/android_reboot.h>
+#include <errno.h>
+#include <stdlib.h>
+#include <sys/mount.h>
+#include <sys/reboot.h>
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <unistd.h>
+
+#include "automounter_gonk.h"
+#include "updatedefines.h"
+#include "updatelogging.h"
+
+#define LOG_TAG "GonkAutoMounter"
+
+#define GONK_LOG(level, format, ...) \
+ LOG((LOG_TAG ": " format "\n", ##__VA_ARGS__)); \
+ __android_log_print(level, LOG_TAG, format, ##__VA_ARGS__)
+
+#define LOGI(format, ...) GONK_LOG(ANDROID_LOG_INFO, format, ##__VA_ARGS__)
+#define LOGE(format, ...) GONK_LOG(ANDROID_LOG_ERROR, format, ##__VA_ARGS__)
+
+const char *kGonkMountsPath = "/proc/mounts";
+const char *kGonkSystemPath = "/system";
+
+GonkAutoMounter::GonkAutoMounter() : mDevice(nullptr), mAccess(Unknown)
+{
+ if (!RemountSystem(ReadWrite)) {
+ LOGE("Could not remount %s as read-write.", kGonkSystemPath);
+ }
+}
+
+GonkAutoMounter::~GonkAutoMounter()
+{
+ bool result = RemountSystem(ReadOnly);
+ free(mDevice);
+
+ if (!result) {
+ // Don't take any chances when remounting as read-only fails, just reboot.
+ Reboot();
+ }
+}
+
+void
+GonkAutoMounter::Reboot()
+{
+ // The android_reboot wrapper provides more safety, doing fancier read-only
+ // remounting and attempting to sync() the filesystem first. If this fails
+ // our only hope is to force a reboot directly without these protections.
+ // For more, see system/core/libcutils/android_reboot.c
+ LOGE("Could not remount %s as read-only, forcing a system reboot.",
+ kGonkSystemPath);
+ LogFlush();
+
+ if (android_reboot(ANDROID_RB_RESTART, 0, nullptr) != 0) {
+ LOGE("Safe system reboot failed, attempting to force");
+ LogFlush();
+
+ if (reboot(RB_AUTOBOOT) != 0) {
+ LOGE("CRITICAL: Failed to force restart");
+ }
+ }
+}
+
+static const char *
+MountAccessToString(MountAccess access)
+{
+ switch (access) {
+ case ReadOnly: return "read-only";
+ case ReadWrite: return "read-write";
+ default: return "unknown";
+ }
+}
+
+bool
+GonkAutoMounter::RemountSystem(MountAccess access)
+{
+ if (!UpdateMountStatus()) {
+ return false;
+ }
+
+ if (mAccess == access) {
+ return true;
+ }
+
+ unsigned long flags = MS_REMOUNT;
+ if (access == ReadOnly) {
+ flags |= MS_RDONLY;
+ // Give the system a chance to flush file buffers
+ sync();
+ }
+
+ if (!MountSystem(flags)) {
+ return false;
+ }
+
+ // Check status again to verify /system has been properly remounted
+ if (!UpdateMountStatus()) {
+ return false;
+ }
+
+ if (mAccess != access) {
+ LOGE("Updated mount status %s should be %s",
+ MountAccessToString(mAccess),
+ MountAccessToString(access));
+ return false;
+ }
+
+ return true;
+}
+
+bool
+GonkAutoMounter::UpdateMountStatus()
+{
+ FILE *mountsFile = NS_tfopen(kGonkMountsPath, "r");
+
+ if (mountsFile == nullptr) {
+ LOGE("Error opening %s: %s", kGonkMountsPath, strerror(errno));
+ return false;
+ }
+
+ // /proc/mounts returns a 0 size from fstat, so we use the same
+ // pre-allocated buffer size that ADB does here
+ const int mountsMaxSize = 4096;
+ char mountData[mountsMaxSize];
+ size_t read = fread(mountData, 1, mountsMaxSize - 1, mountsFile);
+ mountData[read + 1] = '\0';
+
+ if (ferror(mountsFile)) {
+ LOGE("Error reading %s, %s", kGonkMountsPath, strerror(errno));
+ fclose(mountsFile);
+ return false;
+ }
+
+ char *token, *tokenContext;
+ bool foundSystem = false;
+
+ for (token = strtok_r(mountData, "\n", &tokenContext);
+ token;
+ token = strtok_r(nullptr, "\n", &tokenContext))
+ {
+ if (ProcessMount(token)) {
+ foundSystem = true;
+ break;
+ }
+ }
+
+ fclose(mountsFile);
+
+ if (!foundSystem) {
+ LOGE("Couldn't find %s mount in %s", kGonkSystemPath, kGonkMountsPath);
+ }
+ return foundSystem;
+}
+
+bool
+GonkAutoMounter::ProcessMount(const char *mount)
+{
+ const int strSize = 256;
+ char mountDev[strSize];
+ char mountDir[strSize];
+ char mountAccess[strSize];
+
+ int rv = sscanf(mount, "%255s %255s %*s %255s %*d %*d\n",
+ mountDev, mountDir, mountAccess);
+ mountDev[strSize - 1] = '\0';
+ mountDir[strSize - 1] = '\0';
+ mountAccess[strSize - 1] = '\0';
+
+ if (rv != 3) {
+ return false;
+ }
+
+ if (strcmp(kGonkSystemPath, mountDir) != 0) {
+ return false;
+ }
+
+ free(mDevice);
+ mDevice = strdup(mountDev);
+ mAccess = Unknown;
+
+ char *option, *optionContext;
+ for (option = strtok_r(mountAccess, ",", &optionContext);
+ option;
+ option = strtok_r(nullptr, ",", &optionContext))
+ {
+ if (strcmp("ro", option) == 0) {
+ mAccess = ReadOnly;
+ break;
+ } else if (strcmp("rw", option) == 0) {
+ mAccess = ReadWrite;
+ break;
+ }
+ }
+
+ return true;
+}
+
+/*
+ * Mark the given block device as read-write or read-only, using the BLKROSET
+ * ioctl.
+ */
+static void SetBlockReadWriteStatus(const char *blockdev, bool setReadOnly) {
+ int fd;
+ int roMode = setReadOnly ? 1 : 0;
+
+ fd = open(blockdev, O_RDONLY);
+ if (fd < 0) {
+ return;
+ }
+
+ if (ioctl(fd, BLKROSET, &roMode) == -1) {
+ LOGE("Error setting read-only mode on %s to %s: %s", blockdev,
+ setReadOnly ? "true": "false", strerror(errno));
+ }
+ close(fd);
+}
+
+
+bool
+GonkAutoMounter::MountSystem(unsigned long flags)
+{
+ if (!mDevice) {
+ LOGE("No device was found for %s", kGonkSystemPath);
+ return false;
+ }
+
+ // Without setting the block device ro mode to false, we get a permission
+ // denied error while trying to remount it in read-write.
+ SetBlockReadWriteStatus(mDevice, (flags & MS_RDONLY));
+
+ const char *readOnly = flags & MS_RDONLY ? "read-only" : "read-write";
+ int result = mount(mDevice, kGonkSystemPath, "none", flags, nullptr);
+
+ if (result != 0) {
+ LOGE("Error mounting %s as %s: %s", kGonkSystemPath, readOnly,
+ strerror(errno));
+ return false;
+ }
+
+ LOGI("Mounted %s partition as %s", kGonkSystemPath, readOnly);
+ return true;
+}
diff --git a/toolkit/mozapps/update/updater/automounter_gonk.h b/toolkit/mozapps/update/updater/automounter_gonk.h
new file mode 100644
index 000000000..e40cacbc2
--- /dev/null
+++ b/toolkit/mozapps/update/updater/automounter_gonk.h
@@ -0,0 +1,48 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 AUTOMOUNTER_GONK_H__
+#define AUTOMOUNTER_GONK_H__
+
+typedef enum {
+ ReadOnly,
+ ReadWrite,
+ Unknown
+} MountAccess;
+
+/**
+ * This class will remount the /system partition as read-write in Gonk to allow
+ * the updater write access. Upon destruction, /system will be remounted back to
+ * read-only. If something causes /system to remain read-write, this class will
+ * reboot the device and allow the system to mount as read-only.
+ *
+ * Code inspired from AOSP system/core/adb/remount_service.c
+ */
+class GonkAutoMounter
+{
+public:
+ GonkAutoMounter();
+ ~GonkAutoMounter();
+
+ MountAccess GetAccess() const
+ {
+ return mAccess;
+ }
+
+private:
+ bool RemountSystem(MountAccess access);
+ bool ForceRemountReadOnly();
+ bool UpdateMountStatus();
+ bool ProcessMount(const char *mount);
+ bool MountSystem(unsigned long flags);
+ void Reboot();
+
+private:
+ char *mDevice;
+ MountAccess mAccess;
+};
+
+#endif // AUTOMOUNTER_GONK_H__
diff --git a/toolkit/mozapps/update/updater/bspatch.cpp b/toolkit/mozapps/update/updater/bspatch.cpp
new file mode 100644
index 000000000..e632fe3d3
--- /dev/null
+++ b/toolkit/mozapps/update/updater/bspatch.cpp
@@ -0,0 +1,187 @@
+/*-
+ * Copyright 2003,2004 Colin Percival
+ * All rights reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted providing that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+ *
+ * Changelog:
+ * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to
+ * the header, and make all the types 32-bit.
+ * --Benjamin Smedberg <benjamin@smedbergs.us>
+ */
+
+#include "bspatch.h"
+#include "errors.h"
+
+#include <sys/stat.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <fcntl.h>
+#include <string.h>
+#include <limits.h>
+
+#if defined(XP_WIN)
+# include <io.h>
+#else
+# include <unistd.h>
+#endif
+
+#ifdef XP_WIN
+# include <winsock2.h>
+#else
+# include <arpa/inet.h>
+#endif
+
+#ifndef SSIZE_MAX
+# define SSIZE_MAX LONG_MAX
+#endif
+
+int
+MBS_ReadHeader(FILE* file, MBSPatchHeader *header)
+{
+ size_t s = fread(header, 1, sizeof(MBSPatchHeader), file);
+ if (s != sizeof(MBSPatchHeader))
+ return READ_ERROR;
+
+ header->slen = ntohl(header->slen);
+ header->scrc32 = ntohl(header->scrc32);
+ header->dlen = ntohl(header->dlen);
+ header->cblen = ntohl(header->cblen);
+ header->difflen = ntohl(header->difflen);
+ header->extralen = ntohl(header->extralen);
+
+ struct stat hs;
+ s = fstat(fileno(file), &hs);
+ if (s)
+ return READ_ERROR;
+
+ if (memcmp(header->tag, "MBDIFF10", 8) != 0)
+ return UNEXPECTED_BSPATCH_ERROR;
+
+ if (sizeof(MBSPatchHeader) +
+ header->cblen +
+ header->difflen +
+ header->extralen != uint32_t(hs.st_size))
+ return UNEXPECTED_BSPATCH_ERROR;
+
+ return OK;
+}
+
+int
+MBS_ApplyPatch(const MBSPatchHeader *header, FILE* patchFile,
+ unsigned char *fbuffer, FILE* file)
+{
+ unsigned char *fbufend = fbuffer + header->slen;
+
+ unsigned char *buf = (unsigned char*) malloc(header->cblen +
+ header->difflen +
+ header->extralen);
+ if (!buf)
+ return BSPATCH_MEM_ERROR;
+
+ int rv = OK;
+
+ size_t r = header->cblen + header->difflen + header->extralen;
+ unsigned char *wb = buf;
+ while (r) {
+ const size_t count = (r > SSIZE_MAX) ? SSIZE_MAX : r;
+ size_t c = fread(wb, 1, count, patchFile);
+ if (c != count) {
+ rv = READ_ERROR;
+ goto end;
+ }
+
+ r -= c;
+ wb += c;
+ }
+
+ {
+ MBSPatchTriple *ctrlsrc = (MBSPatchTriple*) buf;
+ unsigned char *diffsrc = buf + header->cblen;
+ unsigned char *extrasrc = diffsrc + header->difflen;
+
+ MBSPatchTriple *ctrlend = (MBSPatchTriple*) diffsrc;
+ unsigned char *diffend = extrasrc;
+ unsigned char *extraend = extrasrc + header->extralen;
+
+ do {
+ ctrlsrc->x = ntohl(ctrlsrc->x);
+ ctrlsrc->y = ntohl(ctrlsrc->y);
+ ctrlsrc->z = ntohl(ctrlsrc->z);
+
+#ifdef DEBUG_bsmedberg
+ printf("Applying block:\n"
+ " x: %u\n"
+ " y: %u\n"
+ " z: %i\n",
+ ctrlsrc->x,
+ ctrlsrc->y,
+ ctrlsrc->z);
+#endif
+
+ /* Add x bytes from oldfile to x bytes from the diff block */
+
+ if (fbuffer + ctrlsrc->x > fbufend ||
+ diffsrc + ctrlsrc->x > diffend) {
+ rv = UNEXPECTED_BSPATCH_ERROR;
+ goto end;
+ }
+ for (uint32_t i = 0; i < ctrlsrc->x; ++i) {
+ diffsrc[i] += fbuffer[i];
+ }
+ if ((uint32_t) fwrite(diffsrc, 1, ctrlsrc->x, file) != ctrlsrc->x) {
+ rv = WRITE_ERROR_PATCH_FILE;
+ goto end;
+ }
+ fbuffer += ctrlsrc->x;
+ diffsrc += ctrlsrc->x;
+
+ /* Copy y bytes from the extra block */
+
+ if (extrasrc + ctrlsrc->y > extraend) {
+ rv = UNEXPECTED_BSPATCH_ERROR;
+ goto end;
+ }
+ if ((uint32_t) fwrite(extrasrc, 1, ctrlsrc->y, file) != ctrlsrc->y) {
+ rv = WRITE_ERROR_PATCH_FILE;
+ goto end;
+ }
+ extrasrc += ctrlsrc->y;
+
+ /* "seek" forwards in oldfile by z bytes */
+
+ if (fbuffer + ctrlsrc->z > fbufend) {
+ rv = UNEXPECTED_BSPATCH_ERROR;
+ goto end;
+ }
+ fbuffer += ctrlsrc->z;
+
+ /* and on to the next control block */
+
+ ++ctrlsrc;
+ } while (ctrlsrc < ctrlend);
+ }
+
+end:
+ free(buf);
+ return rv;
+}
diff --git a/toolkit/mozapps/update/updater/bspatch.h b/toolkit/mozapps/update/updater/bspatch.h
new file mode 100644
index 000000000..c24c001cc
--- /dev/null
+++ b/toolkit/mozapps/update/updater/bspatch.h
@@ -0,0 +1,93 @@
+/*-
+ * Copyright 2003,2004 Colin Percival
+ * All rights reserved
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted providing that the following conditions
+ * are met:
+ * 1. Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * 2. 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.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``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 AUTHOR 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.
+ *
+ * Changelog:
+ * 2005-04-26 - Define the header as a C structure, add a CRC32 checksum to
+ * the header, and make all the types 32-bit.
+ * --Benjamin Smedberg <benjamin@smedbergs.us>
+ */
+
+#ifndef bspatch_h__
+#define bspatch_h__
+
+#include <stdint.h>
+#include <stdio.h>
+
+typedef struct MBSPatchHeader_ {
+ /* "MBDIFF10" */
+ char tag[8];
+
+ /* Length of the file to be patched */
+ uint32_t slen;
+
+ /* CRC32 of the file to be patched */
+ uint32_t scrc32;
+
+ /* Length of the result file */
+ uint32_t dlen;
+
+ /* Length of the control block in bytes */
+ uint32_t cblen;
+
+ /* Length of the diff block in bytes */
+ uint32_t difflen;
+
+ /* Length of the extra block in bytes */
+ uint32_t extralen;
+
+ /* Control block (MBSPatchTriple[]) */
+ /* Diff block (binary data) */
+ /* Extra block (binary data) */
+} MBSPatchHeader;
+
+/**
+ * Read the header of a patch file into the MBSPatchHeader structure.
+ *
+ * @param fd Must have been opened for reading, and be at the beginning
+ * of the file.
+ */
+int MBS_ReadHeader(FILE* file, MBSPatchHeader *header);
+
+/**
+ * Apply a patch. This method does not validate the checksum of the original
+ * file: client code should validate the checksum before calling this method.
+ *
+ * @param patchfd Must have been processed by MBS_ReadHeader
+ * @param fbuffer The original file read into a memory buffer of length
+ * header->slen.
+ * @param filefd Must have been opened for writing. Should be truncated
+ * to header->dlen if it is an existing file. The offset
+ * should be at the beginning of the file.
+ */
+int MBS_ApplyPatch(const MBSPatchHeader *header, FILE* patchFile,
+ unsigned char *fbuffer, FILE* file);
+
+typedef struct MBSPatchTriple_ {
+ uint32_t x; /* add x bytes from oldfile to x bytes from the diff block */
+ uint32_t y; /* copy y bytes from the extra block */
+ int32_t z; /* seek forwards in oldfile by z bytes */
+} MBSPatchTriple;
+
+#endif // bspatch_h__
diff --git a/toolkit/mozapps/update/updater/dep1.der b/toolkit/mozapps/update/updater/dep1.der
new file mode 100644
index 000000000..95b4ef38c
--- /dev/null
+++ b/toolkit/mozapps/update/updater/dep1.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/dep2.der b/toolkit/mozapps/update/updater/dep2.der
new file mode 100644
index 000000000..a460d6a16
--- /dev/null
+++ b/toolkit/mozapps/update/updater/dep2.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/gen_cert_header.py b/toolkit/mozapps/update/updater/gen_cert_header.py
new file mode 100644
index 000000000..7ecb15619
--- /dev/null
+++ b/toolkit/mozapps/update/updater/gen_cert_header.py
@@ -0,0 +1,22 @@
+from __future__ import print_function
+
+import binascii
+
+def file_byte_generator(filename, block_size = 512):
+ with open(filename, "rb") as f:
+ while True:
+ block = f.read(block_size)
+ if block:
+ for byte in block:
+ yield byte
+ else:
+ break
+
+def create_header(out_fh, in_filename):
+ assert out_fh.name.endswith('.h')
+ array_name = out_fh.name[:-2] + 'Data'
+ hexified = ["0x" + binascii.hexlify(byte) for byte in file_byte_generator(in_filename)]
+ print("const uint8_t " + array_name + "[] = {", file=out_fh)
+ print(", ".join(hexified), file=out_fh)
+ print("};", file=out_fh)
+ return 0
diff --git a/toolkit/mozapps/update/updater/launchchild_osx.mm b/toolkit/mozapps/update/updater/launchchild_osx.mm
new file mode 100644
index 000000000..5a36ae623
--- /dev/null
+++ b/toolkit/mozapps/update/updater/launchchild_osx.mm
@@ -0,0 +1,384 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <Cocoa/Cocoa.h>
+#include <CoreServices/CoreServices.h>
+#include <crt_externs.h>
+#include <stdlib.h>
+#include <stdio.h>
+#include <spawn.h>
+#include <SystemConfiguration/SystemConfiguration.h>
+#include "readstrings.h"
+
+class MacAutoreleasePool {
+public:
+ MacAutoreleasePool()
+ {
+ mPool = [[NSAutoreleasePool alloc] init];
+ }
+ ~MacAutoreleasePool()
+ {
+ [mPool release];
+ }
+
+private:
+ NSAutoreleasePool* mPool;
+};
+
+void LaunchChild(int argc, const char** argv)
+{
+ MacAutoreleasePool pool;
+
+ @try {
+ NSString* launchPath = [NSString stringWithUTF8String:argv[0]];
+ NSMutableArray* arguments = [NSMutableArray arrayWithCapacity:argc - 1];
+ for (int i = 1; i < argc; i++) {
+ [arguments addObject:[NSString stringWithUTF8String:argv[i]]];
+ }
+ [NSTask launchedTaskWithLaunchPath:launchPath
+ arguments:arguments];
+ } @catch (NSException* e) {
+ NSLog(@"%@: %@", e.name, e.reason);
+ }
+}
+
+void
+LaunchMacPostProcess(const char* aAppBundle)
+{
+ MacAutoreleasePool pool;
+
+ // Launch helper to perform post processing for the update; this is the Mac
+ // analogue of LaunchWinPostProcess (PostUpdateWin).
+ NSString* iniPath = [NSString stringWithUTF8String:aAppBundle];
+ iniPath =
+ [iniPath stringByAppendingPathComponent:@"Contents/Resources/updater.ini"];
+
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ if (![fileManager fileExistsAtPath:iniPath]) {
+ // the file does not exist; there is nothing to run
+ return;
+ }
+
+ int readResult;
+ char values[2][MAX_TEXT_LEN];
+ readResult = ReadStrings([iniPath UTF8String],
+ "ExeRelPath\0ExeArg\0",
+ 2,
+ values,
+ "PostUpdateMac");
+ if (readResult) {
+ return;
+ }
+
+ NSString *exeRelPath = [NSString stringWithUTF8String:values[0]];
+ NSString *exeArg = [NSString stringWithUTF8String:values[1]];
+ if (!exeArg || !exeRelPath) {
+ return;
+ }
+
+ // The path must not traverse directories and it must be a relative path.
+ if ([exeRelPath rangeOfString:@".."].location != NSNotFound ||
+ [exeRelPath rangeOfString:@"./"].location != NSNotFound ||
+ [exeRelPath rangeOfString:@"/"].location == 0) {
+ return;
+ }
+
+ NSString* exeFullPath = [NSString stringWithUTF8String:aAppBundle];
+ exeFullPath = [exeFullPath stringByAppendingPathComponent:exeRelPath];
+
+ char optVals[1][MAX_TEXT_LEN];
+ readResult = ReadStrings([iniPath UTF8String],
+ "ExeAsync\0",
+ 1,
+ optVals,
+ "PostUpdateMac");
+
+ NSTask *task = [[NSTask alloc] init];
+ [task setLaunchPath:exeFullPath];
+ [task setArguments:[NSArray arrayWithObject:exeArg]];
+ [task launch];
+ if (!readResult) {
+ NSString *exeAsync = [NSString stringWithUTF8String:optVals[0]];
+ if ([exeAsync isEqualToString:@"false"]) {
+ [task waitUntilExit];
+ }
+ }
+ // ignore the return value of the task, there's nothing we can do with it
+ [task release];
+}
+
+id ConnectToUpdateServer()
+{
+ MacAutoreleasePool pool;
+
+ id updateServer = nil;
+ BOOL isConnected = NO;
+ int currTry = 0;
+ const int numRetries = 10; // Number of IPC connection retries before
+ // giving up.
+ while (!isConnected && currTry < numRetries) {
+ @try {
+ updateServer = (id)[NSConnection
+ rootProxyForConnectionWithRegisteredName:
+ @"org.mozilla.updater.server"
+ host:nil
+ usingNameServer:[NSSocketPortNameServer sharedInstance]];
+ if (!updateServer ||
+ ![updateServer respondsToSelector:@selector(abort)] ||
+ ![updateServer respondsToSelector:@selector(getArguments)] ||
+ ![updateServer respondsToSelector:@selector(shutdown)]) {
+ NSLog(@"Server doesn't exist or doesn't provide correct selectors.");
+ sleep(1); // Wait 1 second.
+ currTry++;
+ } else {
+ isConnected = YES;
+ }
+ } @catch (NSException* e) {
+ NSLog(@"Encountered exception, retrying: %@: %@", e.name, e.reason);
+ sleep(1); // Wait 1 second.
+ currTry++;
+ }
+ }
+ if (!isConnected) {
+ NSLog(@"Failed to connect to update server after several retries.");
+ return nil;
+ }
+ return updateServer;
+}
+
+void CleanupElevatedMacUpdate(bool aFailureOccurred)
+{
+ MacAutoreleasePool pool;
+
+ id updateServer = ConnectToUpdateServer();
+ if (updateServer) {
+ @try {
+ if (aFailureOccurred) {
+ [updateServer performSelector:@selector(abort)];
+ } else {
+ [updateServer performSelector:@selector(shutdown)];
+ }
+ } @catch (NSException* e) { }
+ }
+
+ NSFileManager* manager = [NSFileManager defaultManager];
+ [manager removeItemAtPath:@"/Library/PrivilegedHelperTools/org.mozilla.updater"
+ error:nil];
+ [manager removeItemAtPath:@"/Library/LaunchDaemons/org.mozilla.updater.plist"
+ error:nil];
+ const char* launchctlArgs[] = {"/bin/launchctl",
+ "remove",
+ "org.mozilla.updater"};
+ // The following call will terminate the current process due to the "remove"
+ // argument in launchctlArgs.
+ LaunchChild(3, launchctlArgs);
+}
+
+// Note: Caller is responsible for freeing argv.
+bool ObtainUpdaterArguments(int* argc, char*** argv)
+{
+ MacAutoreleasePool pool;
+
+ id updateServer = ConnectToUpdateServer();
+ if (!updateServer) {
+ // Let's try our best and clean up.
+ CleanupElevatedMacUpdate(true);
+ return false; // Won't actually get here due to CleanupElevatedMacUpdate.
+ }
+
+ @try {
+ NSArray* updaterArguments =
+ [updateServer performSelector:@selector(getArguments)];
+ *argc = [updaterArguments count];
+ char** tempArgv = (char**)malloc(sizeof(char*) * (*argc));
+ for (int i = 0; i < *argc; i++) {
+ int argLen = [[updaterArguments objectAtIndex:i] length] + 1;
+ tempArgv[i] = (char*)malloc(argLen);
+ strncpy(tempArgv[i], [[updaterArguments objectAtIndex:i] UTF8String],
+ argLen);
+ }
+ *argv = tempArgv;
+ } @catch (NSException* e) {
+ // Let's try our best and clean up.
+ CleanupElevatedMacUpdate(true);
+ return false; // Won't actually get here due to CleanupElevatedMacUpdate.
+ }
+ return true;
+}
+
+/**
+ * The ElevatedUpdateServer is launched from a non-elevated updater process.
+ * It allows an elevated updater process (usually a privileged helper tool) to
+ * connect to it and receive all the necessary arguments to complete a
+ * successful update.
+ */
+@interface ElevatedUpdateServer : NSObject
+{
+ NSArray* mUpdaterArguments;
+ BOOL mShouldKeepRunning;
+ BOOL mAborted;
+}
+- (id)initWithArgs:(NSArray*)args;
+- (BOOL)runServer;
+- (NSArray*)getArguments;
+- (void)abort;
+- (BOOL)wasAborted;
+- (void)shutdown;
+- (BOOL)shouldKeepRunning;
+@end
+
+@implementation ElevatedUpdateServer
+
+- (id)initWithArgs:(NSArray*)args
+{
+ self = [super init];
+ if (!self) {
+ return nil;
+ }
+ mUpdaterArguments = args;
+ mShouldKeepRunning = YES;
+ mAborted = NO;
+ return self;
+}
+
+- (BOOL)runServer
+{
+ NSPort* serverPort = [NSSocketPort port];
+ NSConnection* server = [NSConnection connectionWithReceivePort:serverPort
+ sendPort:serverPort];
+ [server setRootObject:self];
+ if ([server registerName:@"org.mozilla.updater.server"
+ withNameServer:[NSSocketPortNameServer sharedInstance]] == NO) {
+ NSLog(@"Unable to register as DirectoryServer.");
+ NSLog(@"Is another copy running?");
+ return NO;
+ }
+
+ while ([self shouldKeepRunning] &&
+ [[NSRunLoop currentRunLoop] runMode:NSDefaultRunLoopMode
+ beforeDate:[NSDate distantFuture]]);
+ return ![self wasAborted];
+}
+
+- (NSArray*)getArguments
+{
+ return mUpdaterArguments;
+}
+
+- (void)abort
+{
+ mAborted = YES;
+ [self shutdown];
+}
+
+- (BOOL)wasAborted
+{
+ return mAborted;
+}
+
+- (void)shutdown
+{
+ mShouldKeepRunning = NO;
+}
+
+- (BOOL)shouldKeepRunning
+{
+ return mShouldKeepRunning;
+}
+
+@end
+
+bool ServeElevatedUpdate(int argc, const char** argv)
+{
+ MacAutoreleasePool pool;
+
+ NSMutableArray* updaterArguments = [NSMutableArray arrayWithCapacity:argc];
+ for (int i = 0; i < argc; i++) {
+ [updaterArguments addObject:[NSString stringWithUTF8String:argv[i]]];
+ }
+
+ ElevatedUpdateServer* updater =
+ [[ElevatedUpdateServer alloc] initWithArgs:[updaterArguments copy]];
+ bool didSucceed = [updater runServer];
+
+ [updater release];
+ return didSucceed;
+}
+
+bool IsOwnedByGroupAdmin(const char* aAppBundle)
+{
+ MacAutoreleasePool pool;
+
+ NSString* appDir = [NSString stringWithUTF8String:aAppBundle];
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+
+ NSDictionary* attributes = [fileManager attributesOfItemAtPath:appDir
+ error:nil];
+ bool isOwnedByAdmin = false;
+ if (attributes &&
+ [[attributes valueForKey:NSFileGroupOwnerAccountID] intValue] == 80) {
+ isOwnedByAdmin = true;
+ }
+ return isOwnedByAdmin;
+}
+
+void SetGroupOwnershipAndPermissions(const char* aAppBundle)
+{
+ MacAutoreleasePool pool;
+
+ NSString* appDir = [NSString stringWithUTF8String:aAppBundle];
+ NSFileManager* fileManager = [NSFileManager defaultManager];
+ NSError* error = nil;
+ NSArray* paths =
+ [fileManager subpathsOfDirectoryAtPath:appDir
+ error:&error];
+ if (error) {
+ return;
+ }
+
+ // Set group ownership of Firefox.app to 80 ("admin") and permissions to
+ // 0775.
+ if (![fileManager setAttributes:@{ NSFileGroupOwnerAccountID: @(80),
+ NSFilePosixPermissions: @(0775) }
+ ofItemAtPath:appDir
+ error:&error] || error) {
+ return;
+ }
+
+ NSArray* permKeys = [NSArray arrayWithObjects:NSFileGroupOwnerAccountID,
+ NSFilePosixPermissions,
+ nil];
+ // For all descendants of Firefox.app, set group ownership to 80 ("admin") and
+ // ensure write permission for the group.
+ for (NSString* currPath in paths) {
+ NSString* child = [appDir stringByAppendingPathComponent:currPath];
+ NSDictionary* oldAttributes =
+ [fileManager attributesOfItemAtPath:child
+ error:&error];
+ if (error) {
+ return;
+ }
+ // Skip symlinks, since they could be pointing to files outside of the .app
+ // bundle.
+ if ([oldAttributes fileType] == NSFileTypeSymbolicLink) {
+ continue;
+ }
+ NSNumber* oldPerms =
+ (NSNumber*)[oldAttributes valueForKey:NSFilePosixPermissions];
+ NSArray* permObjects =
+ [NSArray arrayWithObjects:
+ [NSNumber numberWithUnsignedLong:80],
+ [NSNumber numberWithUnsignedLong:[oldPerms shortValue] | 020],
+ nil];
+ NSDictionary* attributes = [NSDictionary dictionaryWithObjects:permObjects
+ forKeys:permKeys];
+ if (![fileManager setAttributes:attributes
+ ofItemAtPath:child
+ error:&error] || error) {
+ return;
+ }
+ }
+}
diff --git a/toolkit/mozapps/update/updater/loaddlls.cpp b/toolkit/mozapps/update/updater/loaddlls.cpp
new file mode 100644
index 000000000..b4291a5df
--- /dev/null
+++ b/toolkit/mozapps/update/updater/loaddlls.cpp
@@ -0,0 +1,103 @@
+/* -*- Mode: C++; tab-width: 2; 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 <windows.h>
+
+// Delayed load libraries are loaded when the first symbol is used.
+// The following ensures that we load the delayed loaded libraries from the
+// system directory.
+struct AutoLoadSystemDependencies
+{
+ AutoLoadSystemDependencies()
+ {
+ // Remove the current directory from the search path for dynamically loaded
+ // DLLs as a precaution. This call has no effect for delay load DLLs.
+ SetDllDirectory(L"");
+
+ HMODULE module = ::GetModuleHandleW(L"kernel32.dll");
+ if (module) {
+ // SetDefaultDllDirectories is always available on Windows 8 and above. It
+ // is also available on Windows Vista, Windows Server 2008, and
+ // Windows 7 when MS KB2533623 has been applied.
+ decltype(SetDefaultDllDirectories)* setDefaultDllDirectories =
+ (decltype(SetDefaultDllDirectories)*) GetProcAddress(module, "SetDefaultDllDirectories");
+ if (setDefaultDllDirectories) {
+ setDefaultDllDirectories(LOAD_LIBRARY_SEARCH_SYSTEM32);
+ return;
+ }
+ }
+
+ // When SetDefaultDllDirectories is not available, fallback to preloading
+ // dlls. The order that these are loaded does not matter since they are
+ // loaded using the LOAD_WITH_ALTERED_SEARCH_PATH flag.
+#ifdef HAVE_64BIT_BUILD
+ // DLLs for Firefox x64 on Windows 7 (x64).
+ // Note: dwmapi.dll is preloaded since a crash will try to load it from the
+ // application's directory.
+ static LPCWSTR delayDLLs[] = { L"apphelp.dll",
+ L"cryptbase.dll",
+ L"cryptsp.dll",
+ L"dwmapi.dll",
+ L"mpr.dll",
+ L"ntmarta.dll",
+ L"profapi.dll",
+ L"propsys.dll",
+ L"sspicli.dll",
+ L"wsock32.dll" };
+
+#else
+ // DLLs for Firefox x86 on Windows XP through Windows 7 (x86 and x64).
+ // Note: dwmapi.dll is preloaded since a crash will try to load it from the
+ // application's directory.
+ static LPCWSTR delayDLLs[] = { L"apphelp.dll",
+ L"crypt32.dll",
+ L"cryptbase.dll",
+ L"cryptsp.dll",
+ L"dwmapi.dll",
+ L"mpr.dll",
+ L"msasn1.dll",
+ L"ntmarta.dll",
+ L"profapi.dll",
+ L"propsys.dll",
+ L"psapi.dll",
+ L"secur32.dll",
+ L"sspicli.dll",
+ L"userenv.dll",
+ L"uxtheme.dll",
+ L"ws2_32.dll",
+ L"ws2help.dll",
+ L"wsock32.dll" };
+#endif
+
+ WCHAR systemDirectory[MAX_PATH + 1] = { L'\0' };
+ // If GetSystemDirectory fails we accept that we'll load the DLLs from the
+ // normal search path.
+ GetSystemDirectoryW(systemDirectory, MAX_PATH + 1);
+ size_t systemDirLen = wcslen(systemDirectory);
+
+ // Make the system directory path terminate with a slash
+ if (systemDirectory[systemDirLen - 1] != L'\\' && systemDirLen) {
+ systemDirectory[systemDirLen] = L'\\';
+ ++systemDirLen;
+ // No need to re-null terminate
+ }
+
+ // For each known DLL ensure it is loaded from the system32 directory
+ for (size_t i = 0; i < sizeof(delayDLLs) / sizeof(delayDLLs[0]); ++i) {
+ size_t fileLen = wcslen(delayDLLs[i]);
+ wcsncpy(systemDirectory + systemDirLen, delayDLLs[i],
+ MAX_PATH - systemDirLen);
+ if (systemDirLen + fileLen <= MAX_PATH) {
+ systemDirectory[systemDirLen + fileLen] = L'\0';
+ } else {
+ systemDirectory[MAX_PATH] = L'\0';
+ }
+ LPCWSTR fullModulePath = systemDirectory; // just for code readability
+ // LOAD_WITH_ALTERED_SEARCH_PATH makes a dll look in its own directory for
+ // dependencies and is only available on Win 7 and below.
+ LoadLibraryExW(fullModulePath, nullptr, LOAD_WITH_ALTERED_SEARCH_PATH);
+ }
+ }
+} loadDLLs;
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in
new file mode 100644
index 000000000..a9b9fcba9
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Info.plist.in
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+ <key>CFBundleExecutable</key>
+ <string>org.mozilla.updater</string>
+ <key>CFBundleIconFile</key>
+ <string>updater.icns</string>
+ <key>CFBundleIdentifier</key>
+ <string>org.mozilla.updater</string>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+ <key>CFBundleSignature</key>
+ <string>????</string>
+ <key>CFBundleVersion</key>
+ <string>1.0</string>
+ <key>NSMainNibFile</key>
+ <string>MainMenu</string>
+ <key>NSPrincipalClass</key>
+ <string>NSApplication</string>
+ <key>LSMinimumSystemVersion</key>
+ <string>10.5</string>
+ <key>LSMinimumSystemVersionByArchitecture</key>
+ <dict>
+ <key>i386</key>
+ <string>10.5.0</string>
+ <key>x86_64</key>
+ <string>10.6.0</string>
+ </dict>
+ <key>SMAuthorizedClients</key>
+ <array>
+ <string>identifier "%MOZ_MACBUNDLE_ID%" and ((anchor apple generic and certificate leaf[field.1.2.840.113635.100.6.1.9]) or (anchor apple generic and certificate 1[field.1.2.840.113635.100.6.2.6] and certificate leaf[field.1.2.840.113635.100.6.1.13] and certificate leaf[subject.OU] = "43AQ936H96"))</string>
+ </array>
+</dict>
+</plist>
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo b/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo
new file mode 100644
index 000000000..bd04210fb
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/PkgInfo
@@ -0,0 +1 @@
+APPL???? \ No newline at end of file
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in
new file mode 100644
index 000000000..bca4022e7
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in
@@ -0,0 +1,7 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/* Localized versions of Info.plist keys */
+
+CFBundleName = "%APP_NAME% Software Update";
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
new file mode 100644
index 000000000..6cfb50406
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/classes.nib
@@ -0,0 +1,19 @@
+{
+ IBClasses = (
+ {
+ CLASS = FirstResponder;
+ LANGUAGE = ObjC;
+ SUPERCLASS = NSObject;
+},
+ {
+ CLASS = UpdaterUI;
+ LANGUAGE = ObjC;
+ OUTLETS = {
+ progressBar = NSProgressIndicator;
+ progressTextField = NSTextField;
+ };
+ SUPERCLASS = NSObject;
+}
+ );
+ IBVersion = 1;
+} \ No newline at end of file
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
new file mode 100644
index 000000000..150917837
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/info.nib
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>IBDocumentLocation</key>
+ <string>111 162 356 240 0 0 1440 878 </string>
+ <key>IBEditorPositions</key>
+ <dict>
+ <key>29</key>
+ <string>106 299 84 44 0 0 1440 878 </string>
+ </dict>
+ <key>IBFramework Version</key>
+ <string>489.0</string>
+ <key>IBOpenObjects</key>
+ <array>
+ <integer>21</integer>
+ <integer>29</integer>
+ </array>
+ <key>IBSystem Version</key>
+ <string>10J567</string>
+</dict>
+</plist>
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
new file mode 100644
index 000000000..61ff02600
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/English.lproj/MainMenu.nib/keyedobjects.nib
Binary files differ
diff --git a/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns
new file mode 100644
index 000000000..d7499c669
--- /dev/null
+++ b/toolkit/mozapps/update/updater/macbuild/Contents/Resources/updater.icns
Binary files differ
diff --git a/toolkit/mozapps/update/updater/module.ver b/toolkit/mozapps/update/updater/module.ver
new file mode 100644
index 000000000..771416bb1
--- /dev/null
+++ b/toolkit/mozapps/update/updater/module.ver
@@ -0,0 +1 @@
+WIN32_MODULE_DESCRIPTION=@MOZ_APP_DISPLAYNAME@ Software Updater
diff --git a/toolkit/mozapps/update/updater/moz.build b/toolkit/mozapps/update/updater/moz.build
new file mode 100644
index 000000000..1cca83b5b
--- /dev/null
+++ b/toolkit/mozapps/update/updater/moz.build
@@ -0,0 +1,62 @@
+# -*- 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/.
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ Program('org.mozilla.updater')
+else:
+ Program('updater')
+
+updater_rel_path = ''
+include('updater-common.build')
+if CONFIG['ENABLE_TESTS']:
+ DIRS += ['updater-xpcshell']
+
+CXXFLAGS += CONFIG['MOZ_BZ2_CFLAGS']
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ LDFLAGS += ['-sectcreate',
+ '__TEXT',
+ '__info_plist',
+ TOPOBJDIR + '/dist/bin/Info.plist',
+ '-sectcreate',
+ '__TEXT',
+ '__launchd_plist',
+ SRCDIR + '/Launchd.plist']
+
+GENERATED_FILES = [
+ 'primaryCert.h',
+ 'secondaryCert.h',
+ 'xpcshellCert.h',
+]
+
+primary_cert = GENERATED_FILES['primaryCert.h']
+secondary_cert = GENERATED_FILES['secondaryCert.h']
+
+# This is how the xpcshellCertificate.der file is generated, in case we ever
+# have to regenerate it.
+# ./certutil -L -d modules/libmar/tests/unit/data -n mycert -r > xpcshellCertificate.der
+xpcshell_cert = GENERATED_FILES['xpcshellCert.h']
+
+primary_cert.script = 'gen_cert_header.py:create_header'
+secondary_cert.script = 'gen_cert_header.py:create_header'
+xpcshell_cert.script = 'gen_cert_header.py:create_header'
+
+if CONFIG['MOZ_UPDATE_CHANNEL'] in ('beta', 'release', 'esr'):
+ primary_cert.inputs += ['release_primary.der']
+ secondary_cert.inputs += ['release_secondary.der']
+elif CONFIG['MOZ_UPDATE_CHANNEL'] in ('nightly', 'aurora', 'nightly-elm',
+ 'nightly-profiling', 'nightly-oak',
+ 'nightly-ux'):
+ primary_cert.inputs += ['nightly_aurora_level3_primary.der']
+ secondary_cert.inputs += ['nightly_aurora_level3_secondary.der']
+else:
+ primary_cert.inputs += ['dep1.der']
+ secondary_cert.inputs += ['dep2.der']
+
+xpcshell_cert.inputs += ['xpcshellCertificate.der']
+
+if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
+ FINAL_TARGET_FILES.icons += ['updater.png']
diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der
new file mode 100644
index 000000000..b22124798
--- /dev/null
+++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_primary.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der
new file mode 100644
index 000000000..2dffbd02d
--- /dev/null
+++ b/toolkit/mozapps/update/updater/nightly_aurora_level3_secondary.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/progressui.h b/toolkit/mozapps/update/updater/progressui.h
new file mode 100644
index 000000000..5462815de
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui.h
@@ -0,0 +1,40 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 PROGRESSUI_H__
+#define PROGRESSUI_H__
+
+#include "updatedefines.h"
+
+#if defined(XP_WIN)
+ typedef WCHAR NS_tchar;
+ #define NS_main wmain
+#else
+ typedef char NS_tchar;
+ #define NS_main main
+#endif
+
+// Called to perform any initialization of the widget toolkit
+int InitProgressUI(int *argc, NS_tchar ***argv);
+
+#if defined(XP_WIN)
+ // Called on the main thread at startup
+ int ShowProgressUI(bool indeterminate = false, bool initUIStrings = true);
+ int InitProgressUIStrings();
+#elif defined(XP_MACOSX)
+ // Called on the main thread at startup
+ int ShowProgressUI(bool indeterminate = false);
+#else
+ // Called on the main thread at startup
+ int ShowProgressUI();
+#endif
+// May be called from any thread
+void QuitProgressUI();
+
+// May be called from any thread: progress is a number between 0 and 100
+void UpdateProgressUI(float progress);
+
+#endif // PROGRESSUI_H__
diff --git a/toolkit/mozapps/update/updater/progressui_gonk.cpp b/toolkit/mozapps/update/updater/progressui_gonk.cpp
new file mode 100644
index 000000000..f77d0af63
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui_gonk.cpp
@@ -0,0 +1,53 @@
+/* -*- Mode: C++; tab-width: 8; indent-tabs-mode: nil; c-basic-offset: 2 -*-
+ * vim: sw=2 ts=8 et :
+ */
+/* 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 <assert.h>
+#include <stdio.h>
+
+#include <string>
+
+#include "android/log.h"
+
+#include "progressui.h"
+
+#define LOG(args...) __android_log_print(ANDROID_LOG_INFO, "GeckoUpdater" , ## args)
+
+using namespace std;
+
+int InitProgressUI(int *argc, char ***argv)
+{
+ return 0;
+}
+
+int ShowProgressUI()
+{
+ LOG("Starting to apply update ...\n");
+ return 0;
+}
+
+void QuitProgressUI()
+{
+ LOG("Finished applying update\n");
+}
+
+void UpdateProgressUI(float progress)
+{
+ assert(0.0f <= progress && progress <= 100.0f);
+
+ static const size_t kProgressBarLength = 50;
+ static size_t sLastNumBars;
+ size_t numBars = size_t(float(kProgressBarLength) * progress / 100.0f);
+ if (numBars == sLastNumBars) {
+ return;
+ }
+ sLastNumBars = numBars;
+
+ size_t numSpaces = kProgressBarLength - numBars;
+ string bars(numBars, '=');
+ string spaces(numSpaces, ' ');
+ LOG("Progress [ %s%s ]\n", bars.c_str(), spaces.c_str());
+}
diff --git a/toolkit/mozapps/update/updater/progressui_gtk.cpp b/toolkit/mozapps/update/updater/progressui_gtk.cpp
new file mode 100644
index 000000000..902bc5ac8
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui_gtk.cpp
@@ -0,0 +1,132 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <stdio.h>
+#include <gtk/gtk.h>
+#include <unistd.h>
+#include "mozilla/Sprintf.h"
+#include "progressui.h"
+#include "readstrings.h"
+#include "errors.h"
+
+#define TIMER_INTERVAL 100
+
+static float sProgressVal; // between 0 and 100
+static gboolean sQuit = FALSE;
+static gboolean sEnableUI;
+static guint sTimerID;
+
+static GtkWidget *sWin;
+static GtkWidget *sLabel;
+static GtkWidget *sProgressBar;
+
+static const char *sProgramPath;
+
+static gboolean
+UpdateDialog(gpointer data)
+{
+ if (sQuit)
+ {
+ gtk_widget_hide(sWin);
+ gtk_main_quit();
+ }
+
+ float progress = sProgressVal;
+
+ gtk_progress_bar_set_fraction(GTK_PROGRESS_BAR(sProgressBar),
+ progress / 100.0);
+
+ return TRUE;
+}
+
+static gboolean
+OnDeleteEvent(GtkWidget *widget, GdkEvent *event, gpointer user_data)
+{
+ return TRUE;
+}
+
+int
+InitProgressUI(int *pargc, char ***pargv)
+{
+ sProgramPath = (*pargv)[0];
+
+ sEnableUI = gtk_init_check(pargc, pargv);
+ return 0;
+}
+
+int
+ShowProgressUI()
+{
+ if (!sEnableUI)
+ return -1;
+
+ // Only show the Progress UI if the process is taking a significant amount of
+ // time where a significant amount of time is defined as .5 seconds after
+ // ShowProgressUI is called sProgress is less than 70.
+ usleep(500000);
+
+ if (sQuit || sProgressVal > 70.0f)
+ return 0;
+
+ char ini_path[PATH_MAX];
+ SprintfLiteral(ini_path, "%s.ini", sProgramPath);
+
+ StringTable strings;
+ if (ReadStrings(ini_path, &strings) != OK)
+ return -1;
+
+ sWin = gtk_window_new(GTK_WINDOW_TOPLEVEL);
+ if (!sWin)
+ return -1;
+
+ static GdkPixbuf *pixbuf;
+ char icon_path[PATH_MAX];
+ SprintfLiteral(icon_path, "%s.png", sProgramPath);
+
+ g_signal_connect(G_OBJECT(sWin), "delete_event",
+ G_CALLBACK(OnDeleteEvent), nullptr);
+
+ gtk_window_set_title(GTK_WINDOW(sWin), strings.title);
+ gtk_window_set_type_hint(GTK_WINDOW(sWin), GDK_WINDOW_TYPE_HINT_DIALOG);
+ gtk_window_set_position(GTK_WINDOW(sWin), GTK_WIN_POS_CENTER_ALWAYS);
+ gtk_window_set_resizable(GTK_WINDOW(sWin), FALSE);
+ gtk_window_set_decorated(GTK_WINDOW(sWin), TRUE);
+ gtk_window_set_deletable(GTK_WINDOW(sWin),FALSE);
+ pixbuf = gdk_pixbuf_new_from_file (icon_path, nullptr);
+ gtk_window_set_icon(GTK_WINDOW(sWin), pixbuf);
+ g_object_unref(pixbuf);
+
+ GtkWidget *vbox = gtk_vbox_new(TRUE, 6);
+ sLabel = gtk_label_new(strings.info);
+ gtk_misc_set_alignment(GTK_MISC(sLabel), 0.0f, 0.0f);
+ sProgressBar = gtk_progress_bar_new();
+
+ gtk_box_pack_start(GTK_BOX(vbox), sLabel, FALSE, FALSE, 0);
+ gtk_box_pack_start(GTK_BOX(vbox), sProgressBar, TRUE, TRUE, 0);
+
+ sTimerID = g_timeout_add(TIMER_INTERVAL, UpdateDialog, nullptr);
+
+ gtk_container_set_border_width(GTK_CONTAINER(sWin), 10);
+ gtk_container_add(GTK_CONTAINER(sWin), vbox);
+ gtk_widget_show_all(sWin);
+
+ gtk_main();
+ return 0;
+}
+
+// Called on a background thread
+void
+QuitProgressUI()
+{
+ sQuit = TRUE;
+}
+
+// Called on a background thread
+void
+UpdateProgressUI(float progress)
+{
+ sProgressVal = progress; // 32-bit writes are atomic
+}
diff --git a/toolkit/mozapps/update/updater/progressui_null.cpp b/toolkit/mozapps/update/updater/progressui_null.cpp
new file mode 100644
index 000000000..cb3ac6369
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui_null.cpp
@@ -0,0 +1,25 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "progressui.h"
+
+int InitProgressUI(int *argc, char ***argv)
+{
+ return 0;
+}
+
+int ShowProgressUI()
+{
+ return 0;
+}
+
+void QuitProgressUI()
+{
+}
+
+void UpdateProgressUI(float progress)
+{
+}
diff --git a/toolkit/mozapps/update/updater/progressui_osx.mm b/toolkit/mozapps/update/updater/progressui_osx.mm
new file mode 100644
index 000000000..54c9c41b7
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui_osx.mm
@@ -0,0 +1,144 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+#import <Cocoa/Cocoa.h>
+#include <stdio.h>
+#include <unistd.h>
+#include "mozilla/Sprintf.h"
+#include "progressui.h"
+#include "readstrings.h"
+#include "errors.h"
+
+#define TIMER_INTERVAL 0.2
+
+static float sProgressVal; // between 0 and 100
+static BOOL sQuit = NO;
+static BOOL sIndeterminate = NO;
+static StringTable sLabels;
+static const char *sUpdatePath;
+
+@interface UpdaterUI : NSObject
+{
+ IBOutlet NSProgressIndicator *progressBar;
+ IBOutlet NSTextField *progressTextField;
+}
+@end
+
+@implementation UpdaterUI
+
+-(void)awakeFromNib
+{
+ NSWindow *w = [progressBar window];
+
+ [w setTitle:[NSString stringWithUTF8String:sLabels.title]];
+ [progressTextField setStringValue:[NSString stringWithUTF8String:sLabels.info]];
+
+ NSRect origTextFrame = [progressTextField frame];
+ [progressTextField sizeToFit];
+
+ int widthAdjust = progressTextField.frame.size.width - origTextFrame.size.width;
+
+ if (widthAdjust > 0) {
+ NSRect f;
+ f.size.width = w.frame.size.width + widthAdjust;
+ f.size.height = w.frame.size.height;
+ [w setFrame:f display:YES];
+ }
+
+ [w center];
+
+ [progressBar setIndeterminate:sIndeterminate];
+ [progressBar setDoubleValue:0.0];
+
+ [[NSTimer scheduledTimerWithTimeInterval:TIMER_INTERVAL target:self
+ selector:@selector(updateProgressUI:)
+ userInfo:nil repeats:YES] retain];
+
+ // Make sure we are on top initially
+ [NSApp activateIgnoringOtherApps:YES];
+}
+
+// called when the timer goes off
+-(void)updateProgressUI:(NSTimer *)aTimer
+{
+ if (sQuit) {
+ [aTimer invalidate];
+ [aTimer release];
+
+ // It seems to be necessary to activate and hide ourselves before we stop,
+ // otherwise the "run" method will not return until the user focuses some
+ // other app. The activate step is necessary if we are not the active app.
+ // This is a big hack, but it seems to do the trick.
+ [NSApp activateIgnoringOtherApps:YES];
+ [NSApp hide:self];
+ [NSApp stop:self];
+ }
+
+ float progress = sProgressVal;
+
+ [progressBar setDoubleValue:(double)progress];
+}
+
+// leave this as returning a BOOL instead of NSApplicationTerminateReply
+// for backward compatibility
+- (BOOL)applicationShouldTerminate:(NSApplication *)sender
+{
+ return sQuit;
+}
+
+@end
+
+int
+InitProgressUI(int *pargc, char ***pargv)
+{
+ sUpdatePath = (*pargv)[1];
+
+ return 0;
+}
+
+int
+ShowProgressUI(bool indeterminate)
+{
+ // Only show the Progress UI if the process is taking a significant amount of
+ // time where a significant amount of time is defined as .5 seconds after
+ // ShowProgressUI is called sProgress is less than 70.
+ usleep(500000);
+
+ if (sQuit || sProgressVal > 70.0f)
+ return 0;
+
+ char path[PATH_MAX];
+ SprintfLiteral(path, "%s/updater.ini", sUpdatePath);
+ if (ReadStrings(path, &sLabels) != OK)
+ return -1;
+
+ // Continue the update without showing the Progress UI if any of the supplied
+ // strings are larger than MAX_TEXT_LEN (Bug 628829).
+ if (!(strlen(sLabels.title) < MAX_TEXT_LEN - 1 &&
+ strlen(sLabels.info) < MAX_TEXT_LEN - 1))
+ return -1;
+
+ sIndeterminate = indeterminate;
+ [NSApplication sharedApplication];
+ [NSBundle loadNibNamed:@"MainMenu" owner:NSApp];
+ [NSApp run];
+
+ return 0;
+}
+
+// Called on a background thread
+void
+QuitProgressUI()
+{
+ sQuit = YES;
+}
+
+// Called on a background thread
+void
+UpdateProgressUI(float progress)
+{
+ sProgressVal = progress; // 32-bit writes are atomic
+}
diff --git a/toolkit/mozapps/update/updater/progressui_win.cpp b/toolkit/mozapps/update/updater/progressui_win.cpp
new file mode 100644
index 000000000..89bd71e85
--- /dev/null
+++ b/toolkit/mozapps/update/updater/progressui_win.cpp
@@ -0,0 +1,319 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 <stdio.h>
+#include <windows.h>
+#include <commctrl.h>
+#include <process.h>
+#include <io.h>
+
+#include "resource.h"
+#include "progressui.h"
+#include "readstrings.h"
+#include "errors.h"
+
+#define TIMER_ID 1
+#define TIMER_INTERVAL 100
+
+#define RESIZE_WINDOW(hwnd, extrax, extray) \
+ { \
+ RECT windowSize; \
+ GetWindowRect(hwnd, &windowSize); \
+ SetWindowPos(hwnd, 0, 0, 0, windowSize.right - windowSize.left + extrax, \
+ windowSize.bottom - windowSize.top + extray, \
+ SWP_NOMOVE | SWP_NOZORDER); \
+ }
+
+#define MOVE_WINDOW(hwnd, dx, dy) \
+ { \
+ RECT rc; \
+ POINT pt; \
+ GetWindowRect(hwnd, &rc); \
+ pt.x = rc.left; \
+ pt.y = rc.top; \
+ ScreenToClient(GetParent(hwnd), &pt); \
+ SetWindowPos(hwnd, 0, pt.x + dx, pt.y + dy, 0, 0, \
+ SWP_NOSIZE | SWP_NOZORDER); \
+ }
+
+static float sProgress; // between 0 and 100
+static BOOL sQuit = FALSE;
+static BOOL sIndeterminate = FALSE;
+static StringTable sUIStrings;
+
+static BOOL
+GetStringsFile(WCHAR filename[MAX_PATH])
+{
+ if (!GetModuleFileNameW(nullptr, filename, MAX_PATH))
+ return FALSE;
+
+ WCHAR *dot = wcsrchr(filename, '.');
+ if (!dot || wcsicmp(dot + 1, L"exe"))
+ return FALSE;
+
+ wcscpy(dot + 1, L"ini");
+ return TRUE;
+}
+
+static void
+UpdateDialog(HWND hDlg)
+{
+ int pos = int(sProgress + 0.5f);
+ HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS);
+ SendMessage(hWndPro, PBM_SETPOS, pos, 0L);
+}
+
+// The code in this function is from MSDN:
+// http://msdn.microsoft.com/library/default.asp?url=/library/en-us/winui/winui/windowsuserinterface/windowing/dialogboxes/usingdialogboxes.asp
+static void
+CenterDialog(HWND hDlg)
+{
+ RECT rc, rcOwner, rcDlg;
+
+ // Get the owner window and dialog box rectangles.
+ HWND desktop = GetDesktopWindow();
+
+ GetWindowRect(desktop, &rcOwner);
+ GetWindowRect(hDlg, &rcDlg);
+ CopyRect(&rc, &rcOwner);
+
+ // Offset the owner and dialog box rectangles so that
+ // right and bottom values represent the width and
+ // height, and then offset the owner again to discard
+ // space taken up by the dialog box.
+
+ OffsetRect(&rcDlg, -rcDlg.left, -rcDlg.top);
+ OffsetRect(&rc, -rc.left, -rc.top);
+ OffsetRect(&rc, -rcDlg.right, -rcDlg.bottom);
+
+ // The new position is the sum of half the remaining
+ // space and the owner's original position.
+
+ SetWindowPos(hDlg,
+ HWND_TOP,
+ rcOwner.left + (rc.right / 2),
+ rcOwner.top + (rc.bottom / 2),
+ 0, 0, // ignores size arguments
+ SWP_NOSIZE);
+}
+
+static void
+InitDialog(HWND hDlg)
+{
+ WCHAR szwTitle[MAX_TEXT_LEN];
+ WCHAR szwInfo[MAX_TEXT_LEN];
+
+ MultiByteToWideChar(CP_UTF8, 0, sUIStrings.title, -1, szwTitle,
+ sizeof(szwTitle)/sizeof(szwTitle[0]));
+ MultiByteToWideChar(CP_UTF8, 0, sUIStrings.info, -1, szwInfo,
+ sizeof(szwInfo)/sizeof(szwInfo[0]));
+
+ SetWindowTextW(hDlg, szwTitle);
+ SetWindowTextW(GetDlgItem(hDlg, IDC_INFO), szwInfo);
+
+ // Set dialog icon
+ HICON hIcon = LoadIcon(GetModuleHandle(nullptr),
+ MAKEINTRESOURCE(IDI_DIALOG));
+ if (hIcon)
+ SendMessage(hDlg, WM_SETICON, ICON_BIG, (LPARAM) hIcon);
+
+ HWND hWndPro = GetDlgItem(hDlg, IDC_PROGRESS);
+ SendMessage(hWndPro, PBM_SETRANGE, 0, MAKELPARAM(0, 100));
+ if (sIndeterminate) {
+ LONG_PTR val = GetWindowLongPtr(hWndPro, GWL_STYLE);
+ SetWindowLongPtr(hWndPro, GWL_STYLE, val|PBS_MARQUEE);
+ SendMessage(hWndPro,(UINT) PBM_SETMARQUEE,(WPARAM) TRUE,(LPARAM)50 );
+ }
+
+ // Resize the dialog to fit all of the text if necessary.
+ RECT infoSize, textSize;
+ HWND hWndInfo = GetDlgItem(hDlg, IDC_INFO);
+
+ // Get the control's font for calculating the new size for the control
+ HDC hDCInfo = GetDC(hWndInfo);
+ HFONT hInfoFont, hOldFont = NULL;
+ hInfoFont = (HFONT)SendMessage(hWndInfo, WM_GETFONT, 0, 0);
+
+ if (hInfoFont)
+ hOldFont = (HFONT)SelectObject(hDCInfo, hInfoFont);
+
+ // Measure the space needed for the text on a single line. DT_CALCRECT means
+ // nothing is drawn.
+ if (DrawText(hDCInfo, szwInfo, -1, &textSize,
+ DT_CALCRECT | DT_NOCLIP | DT_SINGLELINE)) {
+ GetClientRect(hWndInfo, &infoSize);
+ SIZE extra;
+ // Calculate the additional space needed for the text by subtracting from
+ // the rectangle returned by DrawText the existing client rectangle's width
+ // and height.
+ extra.cx = (textSize.right - textSize.left) - \
+ (infoSize.right - infoSize.left);
+ extra.cy = (textSize.bottom - textSize.top) - \
+ (infoSize.bottom - infoSize.top);
+ if (extra.cx < 0)
+ extra.cx = 0;
+ if (extra.cy < 0)
+ extra.cy = 0;
+ if ((extra.cx > 0) || (extra.cy > 0)) {
+ RESIZE_WINDOW(hDlg, extra.cx, extra.cy);
+ RESIZE_WINDOW(hWndInfo, extra.cx, extra.cy);
+ RESIZE_WINDOW(hWndPro, extra.cx, 0);
+ MOVE_WINDOW(hWndPro, 0, extra.cy);
+ }
+ }
+
+ if (hOldFont)
+ SelectObject(hDCInfo, hOldFont);
+
+ ReleaseDC(hWndInfo, hDCInfo);
+
+ CenterDialog(hDlg); // make dialog appear in the center of the screen
+
+ SetTimer(hDlg, TIMER_ID, TIMER_INTERVAL, nullptr);
+}
+
+// Message handler for update dialog.
+static LRESULT CALLBACK
+DialogProc(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
+{
+ switch (message)
+ {
+ case WM_INITDIALOG:
+ InitDialog(hDlg);
+ return TRUE;
+
+ case WM_TIMER:
+ if (sQuit) {
+ EndDialog(hDlg, 0);
+ } else {
+ UpdateDialog(hDlg);
+ }
+ return TRUE;
+
+ case WM_COMMAND:
+ return TRUE;
+ }
+ return FALSE;
+}
+
+int
+InitProgressUI(int *argc, WCHAR ***argv)
+{
+ return 0;
+}
+
+/**
+ * Initializes the progress UI strings
+ *
+ * @return 0 on success, -1 on error
+*/
+int
+InitProgressUIStrings() {
+ // If we do not have updater.ini, then we should not bother showing UI.
+ WCHAR filename[MAX_PATH];
+ if (!GetStringsFile(filename)) {
+ return -1;
+ }
+
+ if (_waccess(filename, 04)) {
+ return -1;
+ }
+
+ // If the updater.ini doesn't have the required strings, then we should not
+ // bother showing UI.
+ if (ReadStrings(filename, &sUIStrings) != OK) {
+ return -1;
+ }
+
+ return 0;
+}
+
+int
+ShowProgressUI(bool indeterminate, bool initUIStrings)
+{
+ sIndeterminate = indeterminate;
+ if (!indeterminate) {
+ // Only show the Progress UI if the process is taking a significant amount of
+ // time where a significant amount of time is defined as .5 seconds after
+ // ShowProgressUI is called sProgress is less than 70.
+ Sleep(500);
+
+ if (sQuit || sProgress > 70.0f)
+ return 0;
+ }
+
+ // Don't load the UI if there's an <exe_name>.Local directory for redirection.
+ WCHAR appPath[MAX_PATH + 1] = { L'\0' };
+ if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) {
+ return -1;
+ }
+
+ if (wcslen(appPath) + wcslen(L".Local") >= MAX_PATH) {
+ return -1;
+ }
+
+ wcscat(appPath, L".Local");
+
+ if (!_waccess(appPath, 04)) {
+ return -1;
+ }
+
+ // Don't load the UI if the strings for the UI are not provided.
+ if (initUIStrings && InitProgressUIStrings() == -1) {
+ return -1;
+ }
+
+ if (!GetModuleFileNameW(nullptr, appPath, MAX_PATH)) {
+ return -1;
+ }
+
+ // Use an activation context that supports visual styles for the controls.
+ ACTCTXW actx = {0};
+ actx.cbSize = sizeof(ACTCTXW);
+ actx.dwFlags = ACTCTX_FLAG_RESOURCE_NAME_VALID | ACTCTX_FLAG_HMODULE_VALID;
+ actx.hModule = GetModuleHandle(NULL); // Use the embedded manifest
+ // This is needed only for Win XP but doesn't cause a problem with other
+ // versions of Windows.
+ actx.lpSource = appPath;
+ actx.lpResourceName = MAKEINTRESOURCE(IDR_COMCTL32_MANIFEST);
+
+ HANDLE hactx = INVALID_HANDLE_VALUE;
+ hactx = CreateActCtxW(&actx);
+ ULONG_PTR actxCookie = NULL;
+ if (hactx != INVALID_HANDLE_VALUE) {
+ // Push the specified activation context to the top of the activation stack.
+ ActivateActCtx(hactx, &actxCookie);
+ }
+
+ INITCOMMONCONTROLSEX icc = {
+ sizeof(INITCOMMONCONTROLSEX),
+ ICC_PROGRESS_CLASS
+ };
+ InitCommonControlsEx(&icc);
+
+ DialogBox(GetModuleHandle(nullptr),
+ MAKEINTRESOURCE(IDD_DIALOG), nullptr,
+ (DLGPROC) DialogProc);
+
+ if (hactx != INVALID_HANDLE_VALUE) {
+ // Deactivate the context now that the comctl32.dll is loaded.
+ DeactivateActCtx(0, actxCookie);
+ }
+
+ return 0;
+}
+
+void
+QuitProgressUI()
+{
+ sQuit = TRUE;
+}
+
+void
+UpdateProgressUI(float progress)
+{
+ sProgress = progress; // 32-bit writes are atomic
+}
diff --git a/toolkit/mozapps/update/updater/release_primary.der b/toolkit/mozapps/update/updater/release_primary.der
new file mode 100644
index 000000000..11417c35e
--- /dev/null
+++ b/toolkit/mozapps/update/updater/release_primary.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/release_secondary.der b/toolkit/mozapps/update/updater/release_secondary.der
new file mode 100644
index 000000000..16a7ef6d9
--- /dev/null
+++ b/toolkit/mozapps/update/updater/release_secondary.der
Binary files differ
diff --git a/toolkit/mozapps/update/updater/resource.h b/toolkit/mozapps/update/updater/resource.h
new file mode 100644
index 000000000..3cfa4efda
--- /dev/null
+++ b/toolkit/mozapps/update/updater/resource.h
@@ -0,0 +1,29 @@
+/* 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 IDD_DIALOG 101
+#define IDC_PROGRESS 1000
+#define IDC_INFO 1002
+#define IDI_DIALOG 1003
+#define TYPE_CERT 512
+#define IDR_PRIMARY_CERT 1004
+#define IDR_BACKUP_CERT 1005
+#define IDS_UPDATER_IDENTITY 1006
+#define IDR_XPCSHELL_CERT 1007
+#define IDR_COMCTL32_MANIFEST 17
+
+// 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 1008
+#define _APS_NEXT_SYMED_VALUE 101
+#endif
+#endif
diff --git a/toolkit/mozapps/update/updater/updater-common.build b/toolkit/mozapps/update/updater/updater-common.build
new file mode 100644
index 000000000..02b7338bc
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater-common.build
@@ -0,0 +1,136 @@
+# -*- 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/.
+
+srcs = [
+ 'archivereader.cpp',
+ 'bspatch.cpp',
+ 'updater.cpp',
+]
+
+have_progressui = 0
+
+if CONFIG['MOZ_VERIFY_MAR_SIGNATURE']:
+ USE_LIBS += [
+ 'verifymar',
+ ]
+
+if CONFIG['OS_ARCH'] == 'WINNT':
+ have_progressui = 1
+ srcs += [
+ 'loaddlls.cpp',
+ 'progressui_win.cpp',
+ 'win_dirent.cpp',
+ ]
+ RCINCLUDE = '%supdater.rc' % updater_rel_path
+ DEFINES['UNICODE'] = True
+ DEFINES['_UNICODE'] = True
+ USE_STATIC_LIBS = True
+
+ # Pick up nsWindowsRestart.cpp
+ LOCAL_INCLUDES += [
+ '/toolkit/xre',
+ ]
+ USE_LIBS += [
+ 'updatecommon-standalone',
+ ]
+ OS_LIBS += [
+ 'comctl32',
+ 'ws2_32',
+ 'shell32',
+ 'shlwapi',
+ 'crypt32',
+ 'advapi32',
+ ]
+elif CONFIG['OS_ARCH'] == 'Linux' and CONFIG['MOZ_VERIFY_MAR_SIGNATURE']:
+ USE_LIBS += [
+ 'nss',
+ 'signmar',
+ 'updatecommon',
+ ]
+ OS_LIBS += CONFIG['NSPR_LIBS']
+else:
+ USE_LIBS += [
+ 'updatecommon',
+ ]
+
+USE_LIBS += [
+ 'mar',
+]
+
+if CONFIG['MOZ_SYSTEM_BZ2']:
+ OS_LIBS += CONFIG['MOZ_BZ2_LIBS']
+else:
+ USE_LIBS += [
+ 'bz2',
+ ]
+
+if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
+ have_progressui = 1
+ srcs += [
+ 'progressui_gtk.cpp',
+ ]
+
+if CONFIG['MOZ_WIDGET_TOOLKIT'] == 'cocoa':
+ have_progressui = 1
+ srcs += [
+ 'launchchild_osx.mm',
+ 'progressui_osx.mm',
+ ]
+ OS_LIBS += [
+ '-framework Cocoa',
+ '-framework Security',
+ '-framework SystemConfiguration',
+ ]
+ UNIFIED_SOURCES += [
+ '/toolkit/xre/updaterfileutils_osx.mm',
+ ]
+ LOCAL_INCLUDES += [
+ '/toolkit/xre',
+ ]
+elif CONFIG['MOZ_WIDGET_TOOLKIT'] == 'gonk':
+ have_progressui = 1
+ srcs += [
+ 'automounter_gonk.cpp',
+ 'progressui_gonk.cpp',
+ ]
+ DISABLE_STL_WRAPPING = True
+ OS_LIBS += [
+ 'cutils',
+ 'sysutils',
+ ]
+
+if have_progressui == 0:
+ srcs += [
+ 'progressui_null.cpp',
+ ]
+
+SOURCES += sorted(srcs)
+
+DEFINES['NS_NO_XPCOM'] = True
+DISABLE_STL_WRAPPING = True
+for var in ('MAR_CHANNEL_ID', 'MOZ_APP_VERSION'):
+ DEFINES[var] = '"%s"' % CONFIG[var]
+
+LOCAL_INCLUDES += [
+ '/toolkit/mozapps/update/common',
+ '/xpcom/glue',
+]
+
+DELAYLOAD_DLLS += [
+ 'crypt32.dll',
+ 'comctl32.dll',
+ 'userenv.dll',
+ 'wsock32.dll',
+]
+
+if CONFIG['_MSC_VER']:
+ WIN32_EXE_LDFLAGS += ['-ENTRY:wmainCRTStartup']
+elif CONFIG['OS_ARCH'] == 'WINNT':
+ WIN32_EXE_LDFLAGS += ['-municode']
+
+if 'gtk' in CONFIG['MOZ_WIDGET_TOOLKIT']:
+ CXXFLAGS += CONFIG['TK_CFLAGS']
+ OS_LIBS += CONFIG['TK_LIBS']
diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
new file mode 100644
index 000000000..01822b186
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater-xpcshell/Makefile.in
@@ -0,0 +1,41 @@
+# vim:set ts=8 sw=8 sts=8 noet:
+# 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/.
+
+# For changes here, also consider ../Makefile.in
+
+XPCSHELLTESTROOT = $(topobjdir)/_tests/xpcshell/toolkit/mozapps/update/tests
+MOCHITESTROOT = $(topobjdir)/_tests/testing/mochitest/chrome/toolkit/mozapps/update/tests
+
+include $(topsrcdir)/config/rules.mk
+
+ifndef MOZ_WINCONSOLE
+ifdef MOZ_DEBUG
+MOZ_WINCONSOLE = 1
+else
+MOZ_WINCONSOLE = 0
+endif
+endif
+
+ifdef COMPILE_ENVIRONMENT
+tools::
+ifeq (cocoa,$(MOZ_WIDGET_TOOLKIT))
+ # Copy for xpcshell tests
+ $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
+ rsync -a -C --exclude '*.in' $(srcdir)/../macbuild/Contents $(XPCSHELLTESTROOT)/data/updater-xpcshell.app
+ sed -e 's/%APP_NAME%/$(MOZ_APP_DISPLAYNAME)/' $(srcdir)/../macbuild/Contents/Resources/English.lproj/InfoPlist.strings.in | \
+ iconv -f UTF-8 -t UTF-16 > $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/Resources/English.lproj/InfoPlist.strings
+ $(NSINSTALL) -D $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS/updater-xpcshell
+ $(NSINSTALL) updater-xpcshell $(XPCSHELLTESTROOT)/data/updater-xpcshell.app/Contents/MacOS
+ rm -Rf $(XPCSHELLTESTROOT)/data/updater.app
+ mv $(XPCSHELLTESTROOT)/data/updater-xpcshell.app $(XPCSHELLTESTROOT)/data/updater.app
+ mv $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/updater-xpcshell $(XPCSHELLTESTROOT)/data/updater.app/Contents/MacOS/org.mozilla.updater
+
+ # Copy for mochitest chrome tests
+ rsync -a -C $(XPCSHELLTESTROOT)/data/updater.app $(MOCHITESTROOT)/data/
+else
+ cp $(PROGRAM) $(XPCSHELLTESTROOT)/data/updater$(BIN_SUFFIX)
+ cp $(PROGRAM) $(MOCHITESTROOT)/data/updater$(BIN_SUFFIX)
+endif
+endif # COMPILE_ENVIRONMENT
diff --git a/toolkit/mozapps/update/updater/updater-xpcshell/moz.build b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build
new file mode 100644
index 000000000..710b7e1de
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater-xpcshell/moz.build
@@ -0,0 +1,14 @@
+# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# This Source Code Form is subject to the terms of the Mozilla Public
+# License, v. 2.0. If a copy of the MPL was not distributed with this
+# file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+Program('updater-xpcshell')
+
+updater_rel_path = '../'
+DIST_INSTALL = False
+DEFINES['TEST_UPDATER'] = True
+include('../updater-common.build')
+
+CXXFLAGS += CONFIG['MOZ_BZ2_CFLAGS']
diff --git a/toolkit/mozapps/update/updater/updater.cpp b/toolkit/mozapps/update/updater/updater.cpp
new file mode 100644
index 000000000..63a92c084
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.cpp
@@ -0,0 +1,4454 @@
+/* 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/. */
+
+/**
+ * Manifest Format
+ * ---------------
+ *
+ * contents = 1*( line )
+ * line = method LWS *( param LWS ) CRLF
+ * CRLF = "\r\n"
+ * LWS = 1*( " " | "\t" )
+ *
+ * Available methods for the manifest file:
+ *
+ * updatev2.manifest
+ * -----------------
+ * method = "add" | "add-if" | "patch" | "patch-if" | "remove" |
+ * "rmdir" | "rmrfdir" | type
+ *
+ * 'type' is the update type (e.g. complete or partial) and when present MUST
+ * be the first entry in the update manifest. The type is used to support
+ * downgrades by causing the actions defined in precomplete to be performed.
+ *
+ * updatev3.manifest
+ * -----------------
+ * method = "add" | "add-if" | "add-if-not" | "patch" | "patch-if" |
+ * "remove" | "rmdir" | "rmrfdir" | type
+ *
+ * 'add-if-not' adds a file if it doesn't exist.
+ *
+ * precomplete
+ * -----------
+ * method = "remove" | "rmdir"
+ */
+#include "bspatch.h"
+#include "progressui.h"
+#include "archivereader.h"
+#include "readstrings.h"
+#include "errors.h"
+#include "bzlib.h"
+
+#include <stdio.h>
+#include <string.h>
+#include <stdlib.h>
+#include <stdarg.h>
+
+#include <sys/types.h>
+#include <sys/stat.h>
+#include <fcntl.h>
+#include <limits.h>
+#include <errno.h>
+#include <algorithm>
+
+#include "updatecommon.h"
+#ifdef XP_MACOSX
+#include "updaterfileutils_osx.h"
+#endif // XP_MACOSX
+
+#include "mozilla/Compiler.h"
+#include "mozilla/Types.h"
+#include "mozilla/UniquePtr.h"
+
+// Amount of the progress bar to use in each of the 3 update stages,
+// should total 100.0.
+#define PROGRESS_PREPARE_SIZE 20.0f
+#define PROGRESS_EXECUTE_SIZE 75.0f
+#define PROGRESS_FINISH_SIZE 5.0f
+
+// Amount of time in ms to wait for the parent process to close
+#ifdef DEBUG
+// Use a large value for debug builds since the xpcshell tests take a long time.
+#define PARENT_WAIT 30000
+#else
+#define PARENT_WAIT 10000
+#endif
+
+#if defined(XP_MACOSX)
+// These functions are defined in launchchild_osx.mm
+void CleanupElevatedMacUpdate(bool aFailureOccurred);
+bool IsOwnedByGroupAdmin(const char* aAppBundle);
+bool IsRecursivelyWritable(const char* aPath);
+void LaunchChild(int argc, const char** argv);
+void LaunchMacPostProcess(const char* aAppBundle);
+bool ObtainUpdaterArguments(int* argc, char*** argv);
+bool ServeElevatedUpdate(int argc, const char** argv);
+void SetGroupOwnershipAndPermissions(const char* aAppBundle);
+struct UpdateServerThreadArgs
+{
+ int argc;
+ const NS_tchar** argv;
+};
+#endif
+
+#ifndef _O_BINARY
+# define _O_BINARY 0
+#endif
+
+#ifndef NULL
+# define NULL (0)
+#endif
+
+#ifndef SSIZE_MAX
+# define SSIZE_MAX LONG_MAX
+#endif
+
+// We want to use execv to invoke the callback executable on platforms where
+// we were launched using execv. See nsUpdateDriver.cpp.
+#if defined(XP_UNIX) && !defined(XP_MACOSX)
+#define USE_EXECV
+#endif
+
+#if defined(MOZ_WIDGET_GONK)
+# include "automounter_gonk.h"
+# include <unistd.h>
+# include <android/log.h>
+# include <linux/ioprio.h>
+# include <sys/resource.h>
+
+#if ANDROID_VERSION < 21
+// The only header file in bionic which has a function prototype for ioprio_set
+// is libc/include/sys/linux-unistd.h. However, linux-unistd.h conflicts
+// badly with unistd.h, so we declare the prototype for ioprio_set directly.
+extern "C" MOZ_EXPORT int ioprio_set(int which, int who, int ioprio);
+#else
+# include <sys/syscall.h>
+static int ioprio_set(int which, int who, int ioprio) {
+ return syscall(__NR_ioprio_set, which, who, ioprio);
+}
+#endif
+
+# define MAYBE_USE_HARD_LINKS 1
+static bool sUseHardLinks = true;
+#else
+# define MAYBE_USE_HARD_LINKS 0
+#endif
+
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+ !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
+#include "nss.h"
+#include "prerror.h"
+#endif
+
+#ifdef XP_WIN
+#ifdef MOZ_MAINTENANCE_SERVICE
+#include "registrycertificates.h"
+#endif
+BOOL PathAppendSafe(LPWSTR base, LPCWSTR extra);
+BOOL PathGetSiblingFilePath(LPWSTR destinationBuffer,
+ LPCWSTR siblingFilePath,
+ LPCWSTR newFileName);
+#include "updatehelper.h"
+
+// Closes the handle if valid and if the updater is elevated returns with the
+// return code specified. This prevents multiple launches of the callback
+// application by preventing the elevated process from launching the callback.
+#define EXIT_WHEN_ELEVATED(path, handle, retCode) \
+ { \
+ if (handle != INVALID_HANDLE_VALUE) { \
+ CloseHandle(handle); \
+ } \
+ if (_waccess(path, F_OK) == 0 && NS_tremove(path) != 0) { \
+ LogFinish(); \
+ return retCode; \
+ } \
+ }
+#endif
+
+//-----------------------------------------------------------------------------
+
+// This variable lives in libbz2. It's declared in bzlib_private.h, so we just
+// declare it here to avoid including that entire header file.
+#define BZ2_CRC32TABLE_UNDECLARED
+
+#if MOZ_IS_GCC || defined(__clang__)
+extern "C" __attribute__((visibility("default"))) unsigned int BZ2_crc32Table[256];
+#undef BZ2_CRC32TABLE_UNDECLARED
+#elif defined(__SUNPRO_C) || defined(__SUNPRO_CC)
+extern "C" __global unsigned int BZ2_crc32Table[256];
+#undef BZ2_CRC32TABLE_UNDECLARED
+#endif
+#if defined(BZ2_CRC32TABLE_UNDECLARED)
+extern "C" unsigned int BZ2_crc32Table[256];
+#undef BZ2_CRC32TABLE_UNDECLARED
+#endif
+
+static unsigned int
+crc32(const unsigned char *buf, unsigned int len)
+{
+ unsigned int crc = 0xffffffffL;
+
+ const unsigned char *end = buf + len;
+ for (; buf != end; ++buf)
+ crc = (crc << 8) ^ BZ2_crc32Table[(crc >> 24) ^ *buf];
+
+ crc = ~crc;
+ return crc;
+}
+
+//-----------------------------------------------------------------------------
+
+// A simple stack based container for a FILE struct that closes the
+// file descriptor from its destructor.
+class AutoFile
+{
+public:
+ explicit AutoFile(FILE* file = nullptr)
+ : mFile(file) {
+ }
+
+ ~AutoFile() {
+ if (mFile != nullptr)
+ fclose(mFile);
+ }
+
+ AutoFile &operator=(FILE* file) {
+ if (mFile != 0)
+ fclose(mFile);
+ mFile = file;
+ return *this;
+ }
+
+ operator FILE*() {
+ return mFile;
+ }
+
+ FILE* get() {
+ return mFile;
+ }
+
+private:
+ FILE* mFile;
+};
+
+struct MARChannelStringTable {
+ MARChannelStringTable()
+ {
+ MARChannelID[0] = '\0';
+ }
+
+ char MARChannelID[MAX_TEXT_LEN];
+};
+
+//-----------------------------------------------------------------------------
+
+typedef void (* ThreadFunc)(void *param);
+
+#ifdef XP_WIN
+#include <process.h>
+
+class Thread
+{
+public:
+ int Run(ThreadFunc func, void *param)
+ {
+ mThreadFunc = func;
+ mThreadParam = param;
+
+ unsigned int threadID;
+
+ mThread = (HANDLE) _beginthreadex(nullptr, 0, ThreadMain, this, 0,
+ &threadID);
+
+ return mThread ? 0 : -1;
+ }
+ int Join()
+ {
+ WaitForSingleObject(mThread, INFINITE);
+ CloseHandle(mThread);
+ return 0;
+ }
+private:
+ static unsigned __stdcall ThreadMain(void *p)
+ {
+ Thread *self = (Thread *) p;
+ self->mThreadFunc(self->mThreadParam);
+ return 0;
+ }
+ HANDLE mThread;
+ ThreadFunc mThreadFunc;
+ void *mThreadParam;
+};
+
+#elif defined(XP_UNIX)
+#include <pthread.h>
+
+class Thread
+{
+public:
+ int Run(ThreadFunc func, void *param)
+ {
+ return pthread_create(&thr, nullptr, (void* (*)(void *)) func, param);
+ }
+ int Join()
+ {
+ void *result;
+ return pthread_join(thr, &result);
+ }
+private:
+ pthread_t thr;
+};
+
+#else
+#error "Unsupported platform"
+#endif
+
+//-----------------------------------------------------------------------------
+
+static NS_tchar gPatchDirPath[MAXPATHLEN];
+static NS_tchar gInstallDirPath[MAXPATHLEN];
+static NS_tchar gWorkingDirPath[MAXPATHLEN];
+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
+// The current working directory specified in the command line.
+static NS_tchar* gDestPath;
+static NS_tchar gCallbackRelPath[MAXPATHLEN];
+static NS_tchar gCallbackBackupPath[MAXPATHLEN];
+static NS_tchar gDeleteDirPath[MAXPATHLEN];
+#endif
+
+static const NS_tchar kWhitespace[] = NS_T(" \t");
+static const NS_tchar kNL[] = NS_T("\r\n");
+static const NS_tchar kQuote[] = NS_T("\"");
+
+static inline size_t
+mmin(size_t a, size_t b)
+{
+ return (a > b) ? b : a;
+}
+
+static NS_tchar*
+mstrtok(const NS_tchar *delims, NS_tchar **str)
+{
+ if (!*str || !**str) {
+ *str = nullptr;
+ return nullptr;
+ }
+
+ // skip leading "whitespace"
+ NS_tchar *ret = *str;
+ const NS_tchar *d;
+ do {
+ for (d = delims; *d != NS_T('\0'); ++d) {
+ if (*ret == *d) {
+ ++ret;
+ break;
+ }
+ }
+ } while (*d);
+
+ if (!*ret) {
+ *str = ret;
+ return nullptr;
+ }
+
+ NS_tchar *i = ret;
+ do {
+ for (d = delims; *d != NS_T('\0'); ++d) {
+ if (*i == *d) {
+ *i = NS_T('\0');
+ *str = ++i;
+ return ret;
+ }
+ }
+ ++i;
+ } while (*i);
+
+ *str = nullptr;
+ return ret;
+}
+
+static bool
+EnvHasValue(const char *name)
+{
+ const char *val = getenv(name);
+ return (val && *val);
+}
+
+/**
+ * Coverts a relative update path to a full path.
+ *
+ * @param relpath
+ * The relative path to convert to a full path.
+ * @return valid filesystem full path or nullptr if memory allocation fails.
+ */
+static NS_tchar*
+get_full_path(const NS_tchar *relpath)
+{
+ NS_tchar *destpath = sStagedUpdate ? gWorkingDirPath : gInstallDirPath;
+ size_t lendestpath = NS_tstrlen(destpath);
+ size_t lenrelpath = NS_tstrlen(relpath);
+ NS_tchar *s = new NS_tchar[lendestpath + lenrelpath + 2];
+ if (!s) {
+ return nullptr;
+ }
+
+ NS_tchar *c = s;
+
+ NS_tstrcpy(c, destpath);
+ c += lendestpath;
+ NS_tstrcat(c, NS_T("/"));
+ c++;
+
+ NS_tstrcat(c, relpath);
+ c += lenrelpath;
+ *c = NS_T('\0');
+ return s;
+}
+
+/**
+ * Converts a full update path into a relative path; reverses get_full_path.
+ *
+ * @param fullpath
+ * The absolute path to convert into a relative path.
+ * return pointer to the location within fullpath where the relative path starts
+ * or fullpath itself if it already looks relative.
+ */
+static const NS_tchar*
+get_relative_path(const NS_tchar *fullpath)
+{
+ // If the path isn't absolute, just return it as-is.
+#ifdef XP_WIN
+ if (fullpath[1] != ':' && fullpath[2] != '\\') {
+#else
+ if (fullpath[0] != '/') {
+#endif
+ return fullpath;
+ }
+
+ NS_tchar *prefix = sStagedUpdate ? gWorkingDirPath : gInstallDirPath;
+
+ // If the path isn't long enough to be absolute, return it as-is.
+ if (NS_tstrlen(fullpath) <= NS_tstrlen(prefix)) {
+ return fullpath;
+ }
+
+ return fullpath + NS_tstrlen(prefix) + 1;
+}
+
+/**
+ * Gets the platform specific path and performs simple checks to the path. If
+ * the path checks don't pass nullptr will be returned.
+ *
+ * @param line
+ * The line from the manifest that contains the path.
+ * @param isdir
+ * Whether the path is a directory path. Defaults to false.
+ * @return valid filesystem path or nullptr if the path checks fail.
+ */
+static NS_tchar*
+get_valid_path(NS_tchar **line, bool isdir = false)
+{
+ NS_tchar *path = mstrtok(kQuote, line);
+ if (!path) {
+ LOG(("get_valid_path: unable to determine path: " LOG_S, line));
+ return nullptr;
+ }
+
+ // All paths must be relative from the current working directory
+ if (path[0] == NS_T('/')) {
+ LOG(("get_valid_path: path must be relative: " LOG_S, path));
+ return nullptr;
+ }
+
+#ifdef XP_WIN
+ // All paths must be relative from the current working directory
+ if (path[0] == NS_T('\\') || path[1] == NS_T(':')) {
+ LOG(("get_valid_path: path must be relative: " LOG_S, path));
+ return nullptr;
+ }
+#endif
+
+ if (isdir) {
+ // Directory paths must have a trailing forward slash.
+ if (path[NS_tstrlen(path) - 1] != NS_T('/')) {
+ LOG(("get_valid_path: directory paths must have a trailing forward " \
+ "slash: " LOG_S, path));
+ return nullptr;
+ }
+
+ // Remove the trailing forward slash because stat on Windows will return
+ // ENOENT if the path has a trailing slash.
+ path[NS_tstrlen(path) - 1] = NS_T('\0');
+ }
+
+ // Don't allow relative paths that resolve to a parent directory.
+ if (NS_tstrstr(path, NS_T("..")) != nullptr) {
+ LOG(("get_valid_path: paths must not contain '..': " LOG_S, path));
+ return nullptr;
+ }
+
+ return path;
+}
+
+static NS_tchar*
+get_quoted_path(const NS_tchar *path)
+{
+ size_t lenQuote = NS_tstrlen(kQuote);
+ size_t lenPath = NS_tstrlen(path);
+ size_t len = lenQuote + lenPath + lenQuote + 1;
+
+ NS_tchar *s = (NS_tchar *) malloc(len * sizeof(NS_tchar));
+ if (!s)
+ return nullptr;
+
+ NS_tchar *c = s;
+ NS_tstrcpy(c, kQuote);
+ c += lenQuote;
+ NS_tstrcat(c, path);
+ c += lenPath;
+ NS_tstrcat(c, kQuote);
+ c += lenQuote;
+ *c = NS_T('\0');
+ c++;
+ return s;
+}
+
+static void ensure_write_permissions(const NS_tchar *path)
+{
+#ifdef XP_WIN
+ (void) _wchmod(path, _S_IREAD | _S_IWRITE);
+#else
+ struct stat fs;
+ if (!stat(path, &fs) && !(fs.st_mode & S_IWUSR)) {
+ (void)chmod(path, fs.st_mode | S_IWUSR);
+ }
+#endif
+}
+
+static int ensure_remove(const NS_tchar *path)
+{
+ ensure_write_permissions(path);
+ int rv = NS_tremove(path);
+ if (rv)
+ LOG(("ensure_remove: failed to remove file: " LOG_S ", rv: %d, err: %d",
+ path, rv, errno));
+ return rv;
+}
+
+// Remove the directory pointed to by path and all of its files and sub-directories.
+static int ensure_remove_recursive(const NS_tchar *path,
+ bool continueEnumOnFailure = false)
+{
+ // We use lstat rather than stat here so that we can successfully remove
+ // symlinks.
+ struct NS_tstat_t sInfo;
+ int rv = NS_tlstat(path, &sInfo);
+ if (rv) {
+ // This error is benign
+ return rv;
+ }
+ if (!S_ISDIR(sInfo.st_mode)) {
+ return ensure_remove(path);
+ }
+
+ NS_tDIR *dir;
+ NS_tdirent *entry;
+
+ dir = NS_topendir(path);
+ if (!dir) {
+ LOG(("ensure_remove_recursive: unable to open directory: " LOG_S
+ ", rv: %d, err: %d", path, rv, errno));
+ return rv;
+ }
+
+ while ((entry = NS_treaddir(dir)) != 0) {
+ if (NS_tstrcmp(entry->d_name, NS_T(".")) &&
+ NS_tstrcmp(entry->d_name, NS_T(".."))) {
+ NS_tchar childPath[MAXPATHLEN];
+ NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]),
+ NS_T("%s/%s"), path, entry->d_name);
+ rv = ensure_remove_recursive(childPath);
+ if (rv && !continueEnumOnFailure) {
+ break;
+ }
+ }
+ }
+
+ NS_tclosedir(dir);
+
+ if (rv == OK) {
+ ensure_write_permissions(path);
+ rv = NS_trmdir(path);
+ if (rv) {
+ LOG(("ensure_remove_recursive: unable to remove directory: " LOG_S
+ ", rv: %d, err: %d", path, rv, errno));
+ }
+ }
+ return rv;
+}
+
+static bool is_read_only(const NS_tchar *flags)
+{
+ size_t length = NS_tstrlen(flags);
+ if (length == 0)
+ return false;
+
+ // Make sure the string begins with "r"
+ if (flags[0] != NS_T('r'))
+ return false;
+
+ // Look for "r+" or "r+b"
+ if (length > 1 && flags[1] == NS_T('+'))
+ return false;
+
+ // Look for "rb+"
+ if (NS_tstrcmp(flags, NS_T("rb+")) == 0)
+ return false;
+
+ return true;
+}
+
+static FILE* ensure_open(const NS_tchar *path, const NS_tchar *flags, unsigned int options)
+{
+ ensure_write_permissions(path);
+ FILE* f = NS_tfopen(path, flags);
+ if (is_read_only(flags)) {
+ // Don't attempt to modify the file permissions if the file is being opened
+ // in read-only mode.
+ return f;
+ }
+ if (NS_tchmod(path, options) != 0) {
+ if (f != nullptr) {
+ fclose(f);
+ }
+ return nullptr;
+ }
+ struct NS_tstat_t ss;
+ if (NS_tstat(path, &ss) != 0 || ss.st_mode != options) {
+ if (f != nullptr) {
+ fclose(f);
+ }
+ return nullptr;
+ }
+ return f;
+}
+
+// Ensure that the directory containing this file exists.
+static int ensure_parent_dir(const NS_tchar *path)
+{
+ int rv = OK;
+
+ NS_tchar *slash = (NS_tchar *) NS_tstrrchr(path, NS_T('/'));
+ if (slash) {
+ *slash = NS_T('\0');
+ rv = ensure_parent_dir(path);
+ // Only attempt to create the directory if we're not at the root
+ if (rv == OK && *path) {
+ rv = NS_tmkdir(path, 0755);
+ // If the directory already exists, then ignore the error.
+ if (rv < 0 && errno != EEXIST) {
+ LOG(("ensure_parent_dir: failed to create directory: " LOG_S ", " \
+ "err: %d", path, errno));
+ rv = WRITE_ERROR;
+ } else {
+ rv = OK;
+ }
+ }
+ *slash = NS_T('/');
+ }
+ return rv;
+}
+
+#ifdef XP_UNIX
+static int ensure_copy_symlink(const NS_tchar *path, const NS_tchar *dest)
+{
+ // Copy symlinks by creating a new symlink to the same target
+ NS_tchar target[MAXPATHLEN + 1] = {NS_T('\0')};
+ int rv = readlink(path, target, MAXPATHLEN);
+ if (rv == -1) {
+ LOG(("ensure_copy_symlink: failed to read the link: " LOG_S ", err: %d",
+ path, errno));
+ return READ_ERROR;
+ }
+ rv = symlink(target, dest);
+ if (rv == -1) {
+ LOG(("ensure_copy_symlink: failed to create the new link: " LOG_S ", target: " LOG_S " err: %d",
+ dest, target, errno));
+ return READ_ERROR;
+ }
+ return 0;
+}
+#endif
+
+#if MAYBE_USE_HARD_LINKS
+/*
+ * Creates a hardlink (destFilename) which points to the existing file
+ * (srcFilename).
+ *
+ * @return 0 if successful, an error otherwise
+ */
+
+static int
+create_hard_link(const NS_tchar *srcFilename, const NS_tchar *destFilename)
+{
+ if (link(srcFilename, destFilename) < 0) {
+ LOG(("link(%s, %s) failed errno = %d", srcFilename, destFilename, errno));
+ return WRITE_ERROR;
+ }
+ return OK;
+}
+#endif
+
+// Copy the file named path onto a new file named dest.
+static int ensure_copy(const NS_tchar *path, const NS_tchar *dest)
+{
+#ifdef XP_WIN
+ // Fast path for Windows
+ bool result = CopyFileW(path, dest, false);
+ if (!result) {
+ LOG(("ensure_copy: failed to copy the file " LOG_S " over to " LOG_S ", lasterr: %x",
+ path, dest, GetLastError()));
+ return WRITE_ERROR_FILE_COPY;
+ }
+ return OK;
+#else
+ struct NS_tstat_t ss;
+ int rv = NS_tlstat(path, &ss);
+ if (rv) {
+ LOG(("ensure_copy: failed to read file status info: " LOG_S ", err: %d",
+ path, errno));
+ return READ_ERROR;
+ }
+
+#ifdef XP_UNIX
+ if (S_ISLNK(ss.st_mode)) {
+ return ensure_copy_symlink(path, dest);
+ }
+#endif
+
+#if MAYBE_USE_HARD_LINKS
+ if (sUseHardLinks) {
+ if (!create_hard_link(path, dest)) {
+ return OK;
+ }
+ // Since we failed to create the hard link, fall through and copy the file.
+ sUseHardLinks = false;
+ }
+#endif
+
+ AutoFile infile(ensure_open(path, NS_T("rb"), ss.st_mode));
+ if (!infile) {
+ LOG(("ensure_copy: failed to open the file for reading: " LOG_S ", err: %d",
+ path, errno));
+ return READ_ERROR;
+ }
+ AutoFile outfile(ensure_open(dest, NS_T("wb"), ss.st_mode));
+ if (!outfile) {
+ LOG(("ensure_copy: failed to open the file for writing: " LOG_S ", err: %d",
+ dest, errno));
+ return WRITE_ERROR;
+ }
+
+ // This block size was chosen pretty arbitrarily but seems like a reasonable
+ // compromise. For example, the optimal block size on a modern OS X machine
+ // is 100k */
+ const int blockSize = 32 * 1024;
+ void* buffer = malloc(blockSize);
+ if (!buffer)
+ return UPDATER_MEM_ERROR;
+
+ while (!feof(infile.get())) {
+ size_t read = fread(buffer, 1, blockSize, infile);
+ if (ferror(infile.get())) {
+ LOG(("ensure_copy: failed to read the file: " LOG_S ", err: %d",
+ path, errno));
+ free(buffer);
+ return READ_ERROR;
+ }
+
+ size_t written = 0;
+
+ while (written < read) {
+ size_t chunkWritten = fwrite(buffer, 1, read - written, outfile);
+ if (chunkWritten <= 0) {
+ LOG(("ensure_copy: failed to write the file: " LOG_S ", err: %d",
+ dest, errno));
+ free(buffer);
+ return WRITE_ERROR_FILE_COPY;
+ }
+
+ written += chunkWritten;
+ }
+ }
+
+ rv = NS_tchmod(dest, ss.st_mode);
+
+ free(buffer);
+ return rv;
+#endif
+}
+
+template <unsigned N>
+struct copy_recursive_skiplist {
+ NS_tchar paths[N][MAXPATHLEN];
+
+ void append(unsigned index, const NS_tchar *path, const NS_tchar *suffix) {
+ NS_tsnprintf(paths[index], MAXPATHLEN, NS_T("%s/%s"), path, suffix);
+ }
+
+ bool find(const NS_tchar *path) {
+ for (int i = 0; i < static_cast<int>(N); ++i) {
+ if (!NS_tstricmp(paths[i], path)) {
+ return true;
+ }
+ }
+ return false;
+ }
+};
+
+// Copy all of the files and subdirectories under path to a new directory named dest.
+// The path names in the skiplist will be skipped and will not be copied.
+template <unsigned N>
+static int ensure_copy_recursive(const NS_tchar *path, const NS_tchar *dest,
+ copy_recursive_skiplist<N>& skiplist)
+{
+ struct NS_tstat_t sInfo;
+ int rv = NS_tlstat(path, &sInfo);
+ if (rv) {
+ LOG(("ensure_copy_recursive: path doesn't exist: " LOG_S ", rv: %d, err: %d",
+ path, rv, errno));
+ return READ_ERROR;
+ }
+
+#ifdef XP_UNIX
+ if (S_ISLNK(sInfo.st_mode)) {
+ return ensure_copy_symlink(path, dest);
+ }
+#endif
+
+ if (!S_ISDIR(sInfo.st_mode)) {
+ return ensure_copy(path, dest);
+ }
+
+ rv = NS_tmkdir(dest, sInfo.st_mode);
+ if (rv < 0 && errno != EEXIST) {
+ LOG(("ensure_copy_recursive: could not create destination directory: " LOG_S ", rv: %d, err: %d",
+ path, rv, errno));
+ return WRITE_ERROR;
+ }
+
+ NS_tDIR *dir;
+ NS_tdirent *entry;
+
+ dir = NS_topendir(path);
+ if (!dir) {
+ LOG(("ensure_copy_recursive: path is not a directory: " LOG_S ", rv: %d, err: %d",
+ path, rv, errno));
+ return READ_ERROR;
+ }
+
+ while ((entry = NS_treaddir(dir)) != 0) {
+ if (NS_tstrcmp(entry->d_name, NS_T(".")) &&
+ NS_tstrcmp(entry->d_name, NS_T(".."))) {
+ NS_tchar childPath[MAXPATHLEN];
+ NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]),
+ NS_T("%s/%s"), path, entry->d_name);
+ if (skiplist.find(childPath)) {
+ continue;
+ }
+ NS_tchar childPathDest[MAXPATHLEN];
+ NS_tsnprintf(childPathDest, sizeof(childPathDest)/sizeof(childPathDest[0]),
+ NS_T("%s/%s"), dest, entry->d_name);
+ rv = ensure_copy_recursive(childPath, childPathDest, skiplist);
+ if (rv) {
+ break;
+ }
+ }
+ }
+ NS_tclosedir(dir);
+ return rv;
+}
+
+// Renames the specified file to the new file specified. If the destination file
+// exists it is removed.
+static int rename_file(const NS_tchar *spath, const NS_tchar *dpath,
+ bool allowDirs = false)
+{
+ int rv = ensure_parent_dir(dpath);
+ if (rv)
+ return rv;
+
+ struct NS_tstat_t spathInfo;
+ rv = NS_tstat(spath, &spathInfo);
+ if (rv) {
+ LOG(("rename_file: failed to read file status info: " LOG_S ", " \
+ "err: %d", spath, errno));
+ return READ_ERROR;
+ }
+
+ if (!S_ISREG(spathInfo.st_mode)) {
+ if (allowDirs && !S_ISDIR(spathInfo.st_mode)) {
+ LOG(("rename_file: path present, but not a file: " LOG_S ", err: %d",
+ spath, errno));
+ return RENAME_ERROR_EXPECTED_FILE;
+ } else {
+ LOG(("rename_file: proceeding to rename the directory"));
+ }
+ }
+
+ if (!NS_taccess(dpath, F_OK)) {
+ if (ensure_remove(dpath)) {
+ LOG(("rename_file: destination file exists and could not be " \
+ "removed: " LOG_S, dpath));
+ return WRITE_ERROR_DELETE_FILE;
+ }
+ }
+
+ if (NS_trename(spath, dpath) != 0) {
+ LOG(("rename_file: failed to rename file - src: " LOG_S ", " \
+ "dst:" LOG_S ", err: %d", spath, dpath, errno));
+ return WRITE_ERROR;
+ }
+
+ return OK;
+}
+
+#ifdef XP_WIN
+// Remove the directory pointed to by path and all of its files and
+// sub-directories. If a file is in use move it to the tobedeleted directory
+// and attempt to schedule removal of the file on reboot
+static int remove_recursive_on_reboot(const NS_tchar *path, const NS_tchar *deleteDir)
+{
+ struct NS_tstat_t sInfo;
+ int rv = NS_tlstat(path, &sInfo);
+ if (rv) {
+ // This error is benign
+ return rv;
+ }
+
+ if (!S_ISDIR(sInfo.st_mode)) {
+ NS_tchar tmpDeleteFile[MAXPATHLEN];
+ GetTempFileNameW(deleteDir, L"rep", 0, tmpDeleteFile);
+ NS_tremove(tmpDeleteFile);
+ rv = rename_file(path, tmpDeleteFile, false);
+ if (MoveFileEx(rv ? path : tmpDeleteFile, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) {
+ LOG(("remove_recursive_on_reboot: file will be removed on OS reboot: "
+ LOG_S, rv ? path : tmpDeleteFile));
+ } else {
+ LOG(("remove_recursive_on_reboot: failed to schedule OS reboot removal of "
+ "file: " LOG_S, rv ? path : tmpDeleteFile));
+ }
+ return rv;
+ }
+
+ NS_tDIR *dir;
+ NS_tdirent *entry;
+
+ dir = NS_topendir(path);
+ if (!dir) {
+ LOG(("remove_recursive_on_reboot: unable to open directory: " LOG_S
+ ", rv: %d, err: %d",
+ path, rv, errno));
+ return rv;
+ }
+
+ while ((entry = NS_treaddir(dir)) != 0) {
+ if (NS_tstrcmp(entry->d_name, NS_T(".")) &&
+ NS_tstrcmp(entry->d_name, NS_T(".."))) {
+ NS_tchar childPath[MAXPATHLEN];
+ NS_tsnprintf(childPath, sizeof(childPath)/sizeof(childPath[0]),
+ NS_T("%s/%s"), path, entry->d_name);
+ // There is no need to check the return value of this call since this
+ // function is only called after an update is successful and there is not
+ // much that can be done to recover if it isn't successful. There is also
+ // no need to log the value since it will have already been logged.
+ remove_recursive_on_reboot(childPath, deleteDir);
+ }
+ }
+
+ NS_tclosedir(dir);
+
+ if (rv == OK) {
+ ensure_write_permissions(path);
+ rv = NS_trmdir(path);
+ if (rv) {
+ LOG(("remove_recursive_on_reboot: unable to remove directory: " LOG_S
+ ", rv: %d, err: %d", path, rv, errno));
+ }
+ }
+ return rv;
+}
+#endif
+
+//-----------------------------------------------------------------------------
+
+// Create a backup of the specified file by renaming it.
+static int backup_create(const NS_tchar *path)
+{
+ NS_tchar backup[MAXPATHLEN];
+ NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]),
+ NS_T("%s") BACKUP_EXT, path);
+
+ return rename_file(path, backup);
+}
+
+// Rename the backup of the specified file that was created by renaming it back
+// to the original file.
+static int backup_restore(const NS_tchar *path, const NS_tchar *relPath)
+{
+ NS_tchar backup[MAXPATHLEN];
+ NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]),
+ NS_T("%s") BACKUP_EXT, path);
+
+ NS_tchar relBackup[MAXPATHLEN];
+ NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]),
+ NS_T("%s") BACKUP_EXT, relPath);
+
+ if (NS_taccess(backup, F_OK)) {
+ LOG(("backup_restore: backup file doesn't exist: " LOG_S, relBackup));
+ return OK;
+ }
+
+ return rename_file(backup, path);
+}
+
+// Discard the backup of the specified file that was created by renaming it.
+static int backup_discard(const NS_tchar *path, const NS_tchar *relPath)
+{
+ NS_tchar backup[MAXPATHLEN];
+ NS_tsnprintf(backup, sizeof(backup)/sizeof(backup[0]),
+ NS_T("%s") BACKUP_EXT, path);
+
+ NS_tchar relBackup[MAXPATHLEN];
+ NS_tsnprintf(relBackup, sizeof(relBackup) / sizeof(relBackup[0]),
+ NS_T("%s") BACKUP_EXT, relPath);
+
+ // Nothing to discard
+ if (NS_taccess(backup, F_OK)) {
+ return OK;
+ }
+
+ int rv = ensure_remove(backup);
+#if defined(XP_WIN)
+ if (rv && !sStagedUpdate && !sReplaceRequest) {
+ LOG(("backup_discard: unable to remove: " LOG_S, relBackup));
+ NS_tchar path[MAXPATHLEN];
+ GetTempFileNameW(gDeleteDirPath, L"moz", 0, path);
+ if (rename_file(backup, path)) {
+ LOG(("backup_discard: failed to rename file:" LOG_S ", dst:" LOG_S,
+ relBackup, relPath));
+ return WRITE_ERROR_DELETE_BACKUP;
+ }
+ // The MoveFileEx call to remove the file on OS reboot will fail if the
+ // process doesn't have write access to the HKEY_LOCAL_MACHINE registry key
+ // but this is ok since the installer / uninstaller will delete the
+ // directory containing the file along with its contents after an update is
+ // applied, on reinstall, and on uninstall.
+ if (MoveFileEx(path, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) {
+ LOG(("backup_discard: file renamed and will be removed on OS " \
+ "reboot: " LOG_S, relPath));
+ } else {
+ LOG(("backup_discard: failed to schedule OS reboot removal of " \
+ "file: " LOG_S, relPath));
+ }
+ }
+#else
+ if (rv)
+ return WRITE_ERROR_DELETE_BACKUP;
+#endif
+
+ return OK;
+}
+
+// Helper function for post-processing a temporary backup.
+static void backup_finish(const NS_tchar *path, const NS_tchar *relPath,
+ int status)
+{
+ if (status == OK)
+ backup_discard(path, relPath);
+ else
+ backup_restore(path, relPath);
+}
+
+//-----------------------------------------------------------------------------
+
+static int DoUpdate();
+
+class Action
+{
+public:
+ Action() : mProgressCost(1), mNext(nullptr) { }
+ virtual ~Action() { }
+
+ virtual int Parse(NS_tchar *line) = 0;
+
+ // Do any preprocessing to ensure that the action can be performed. Execute
+ // will be called if this Action and all others return OK from this method.
+ virtual int Prepare() = 0;
+
+ // Perform the operation. Return OK to indicate success. After all actions
+ // have been executed, Finish will be called. A requirement of Execute is
+ // that its operation be reversable from Finish.
+ virtual int Execute() = 0;
+
+ // Finish is called after execution of all actions. If status is OK, then
+ // all actions were successfully executed. Otherwise, some action failed.
+ virtual void Finish(int status) = 0;
+
+ int mProgressCost;
+private:
+ Action* mNext;
+
+ friend class ActionList;
+};
+
+class RemoveFile : public Action
+{
+public:
+ RemoveFile() : mSkip(0) { }
+
+ int Parse(NS_tchar *line);
+ int Prepare();
+ int Execute();
+ void Finish(int status);
+
+private:
+ mozilla::UniquePtr<NS_tchar[]> mFile;
+ mozilla::UniquePtr<NS_tchar[]> mRelPath;
+ int mSkip;
+};
+
+int
+RemoveFile::Parse(NS_tchar *line)
+{
+ // format "<deadfile>"
+
+ NS_tchar * validPath = get_valid_path(&line);
+ if (!validPath) {
+ return PARSE_ERROR;
+ }
+
+ mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
+ NS_tstrcpy(mRelPath.get(), validPath);
+
+ mFile.reset(get_full_path(validPath));
+ if (!mFile) {
+ return PARSE_ERROR;
+ }
+
+ return OK;
+}
+
+int
+RemoveFile::Prepare()
+{
+ // Skip the file if it already doesn't exist.
+ int rv = NS_taccess(mFile.get(), F_OK);
+ if (rv) {
+ mSkip = 1;
+ mProgressCost = 0;
+ return OK;
+ }
+
+ LOG(("PREPARE REMOVEFILE " LOG_S, mRelPath.get()));
+
+ // Make sure that we're actually a file...
+ struct NS_tstat_t fileInfo;
+ rv = NS_tstat(mFile.get(), &fileInfo);
+ if (rv) {
+ LOG(("failed to read file status info: " LOG_S ", err: %d", mFile.get(),
+ errno));
+ return READ_ERROR;
+ }
+
+ if (!S_ISREG(fileInfo.st_mode)) {
+ LOG(("path present, but not a file: " LOG_S, mFile.get()));
+ return DELETE_ERROR_EXPECTED_FILE;
+ }
+
+ NS_tchar *slash = (NS_tchar *) NS_tstrrchr(mFile.get(), NS_T('/'));
+ if (slash) {
+ *slash = NS_T('\0');
+ rv = NS_taccess(mFile.get(), W_OK);
+ *slash = NS_T('/');
+ } else {
+ rv = NS_taccess(NS_T("."), W_OK);
+ }
+
+ if (rv) {
+ LOG(("access failed: %d", errno));
+ return WRITE_ERROR_FILE_ACCESS_DENIED;
+ }
+
+ return OK;
+}
+
+int
+RemoveFile::Execute()
+{
+ if (mSkip)
+ return OK;
+
+ LOG(("EXECUTE REMOVEFILE " LOG_S, mRelPath.get()));
+
+ // The file is checked for existence here and in Prepare since it might have
+ // been removed by a separate instruction: bug 311099.
+ int rv = NS_taccess(mFile.get(), F_OK);
+ if (rv) {
+ LOG(("file cannot be removed because it does not exist; skipping"));
+ mSkip = 1;
+ return OK;
+ }
+
+ if (sStagedUpdate) {
+ // Staged updates don't need backup files so just remove it.
+ rv = ensure_remove(mFile.get());
+ if (rv) {
+ return rv;
+ }
+ } else {
+ // Rename the old file. It will be removed in Finish.
+ rv = backup_create(mFile.get());
+ if (rv) {
+ LOG(("backup_create failed: %d", rv));
+ return rv;
+ }
+ }
+
+ return OK;
+}
+
+void
+RemoveFile::Finish(int status)
+{
+ if (mSkip) {
+ return;
+ }
+
+ LOG(("FINISH REMOVEFILE " LOG_S, mRelPath.get()));
+
+ // Staged updates don't create backup files.
+ if (!sStagedUpdate) {
+ backup_finish(mFile.get(), mRelPath.get(), status);
+ }
+}
+
+class RemoveDir : public Action
+{
+public:
+ RemoveDir() : mSkip(0) { }
+
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare(); // check that the source dir exists
+ virtual int Execute();
+ virtual void Finish(int status);
+
+private:
+ mozilla::UniquePtr<NS_tchar[]> mDir;
+ mozilla::UniquePtr<NS_tchar[]> mRelPath;
+ int mSkip;
+};
+
+int
+RemoveDir::Parse(NS_tchar *line)
+{
+ // format "<deaddir>/"
+
+ NS_tchar * validPath = get_valid_path(&line, true);
+ if (!validPath) {
+ return PARSE_ERROR;
+ }
+
+ mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
+ NS_tstrcpy(mRelPath.get(), validPath);
+
+ mDir.reset(get_full_path(validPath));
+ if (!mDir) {
+ return PARSE_ERROR;
+ }
+
+ return OK;
+}
+
+int
+RemoveDir::Prepare()
+{
+ // We expect the directory to exist if we are to remove it.
+ int rv = NS_taccess(mDir.get(), F_OK);
+ if (rv) {
+ mSkip = 1;
+ mProgressCost = 0;
+ return OK;
+ }
+
+ LOG(("PREPARE REMOVEDIR " LOG_S "/", mRelPath.get()));
+
+ // Make sure that we're actually a dir.
+ struct NS_tstat_t dirInfo;
+ rv = NS_tstat(mDir.get(), &dirInfo);
+ if (rv) {
+ LOG(("failed to read directory status info: " LOG_S ", err: %d", mRelPath.get(),
+ errno));
+ return READ_ERROR;
+ }
+
+ if (!S_ISDIR(dirInfo.st_mode)) {
+ LOG(("path present, but not a directory: " LOG_S, mRelPath.get()));
+ return DELETE_ERROR_EXPECTED_DIR;
+ }
+
+ rv = NS_taccess(mDir.get(), W_OK);
+ if (rv) {
+ LOG(("access failed: %d, %d", rv, errno));
+ return WRITE_ERROR_DIR_ACCESS_DENIED;
+ }
+
+ return OK;
+}
+
+int
+RemoveDir::Execute()
+{
+ if (mSkip)
+ return OK;
+
+ LOG(("EXECUTE REMOVEDIR " LOG_S "/", mRelPath.get()));
+
+ // The directory is checked for existence at every step since it might have
+ // been removed by a separate instruction: bug 311099.
+ int rv = NS_taccess(mDir.get(), F_OK);
+ if (rv) {
+ LOG(("directory no longer exists; skipping"));
+ mSkip = 1;
+ }
+
+ return OK;
+}
+
+void
+RemoveDir::Finish(int status)
+{
+ if (mSkip || status != OK)
+ return;
+
+ LOG(("FINISH REMOVEDIR " LOG_S "/", mRelPath.get()));
+
+ // The directory is checked for existence at every step since it might have
+ // been removed by a separate instruction: bug 311099.
+ int rv = NS_taccess(mDir.get(), F_OK);
+ if (rv) {
+ LOG(("directory no longer exists; skipping"));
+ return;
+ }
+
+
+ if (status == OK) {
+ if (NS_trmdir(mDir.get())) {
+ LOG(("non-fatal error removing directory: " LOG_S "/, rv: %d, err: %d",
+ mRelPath.get(), rv, errno));
+ }
+ }
+}
+
+class AddFile : public Action
+{
+public:
+ AddFile() : mAdded(false) { }
+
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare();
+ virtual int Execute();
+ virtual void Finish(int status);
+
+private:
+ mozilla::UniquePtr<NS_tchar[]> mFile;
+ mozilla::UniquePtr<NS_tchar[]> mRelPath;
+ bool mAdded;
+};
+
+int
+AddFile::Parse(NS_tchar *line)
+{
+ // format "<newfile>"
+
+ NS_tchar * validPath = get_valid_path(&line);
+ if (!validPath) {
+ return PARSE_ERROR;
+ }
+
+ mRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
+ NS_tstrcpy(mRelPath.get(), validPath);
+
+ mFile.reset(get_full_path(validPath));
+ if (!mFile) {
+ return PARSE_ERROR;
+ }
+
+ return OK;
+}
+
+int
+AddFile::Prepare()
+{
+ LOG(("PREPARE ADD " LOG_S, mRelPath.get()));
+
+ return OK;
+}
+
+int
+AddFile::Execute()
+{
+ LOG(("EXECUTE ADD " LOG_S, mRelPath.get()));
+
+ int rv;
+
+ // First make sure that we can actually get rid of any existing file.
+ rv = NS_taccess(mFile.get(), F_OK);
+ if (rv == 0) {
+ if (sStagedUpdate) {
+ // Staged updates don't need backup files so just remove it.
+ rv = ensure_remove(mFile.get());
+ } else {
+ rv = backup_create(mFile.get());
+ }
+ if (rv) {
+ return rv;
+ }
+ } else {
+ rv = ensure_parent_dir(mFile.get());
+ if (rv)
+ return rv;
+ }
+
+#ifdef XP_WIN
+ char sourcefile[MAXPATHLEN];
+ if (!WideCharToMultiByte(CP_UTF8, 0, mRelPath.get(), -1, sourcefile,
+ MAXPATHLEN, nullptr, nullptr)) {
+ LOG(("error converting wchar to utf8: %d", GetLastError()));
+ return STRING_CONVERSION_ERROR;
+ }
+
+ rv = gArchiveReader.ExtractFile(sourcefile, mFile.get());
+#else
+ rv = gArchiveReader.ExtractFile(mRelPath.get(), mFile.get());
+#endif
+ if (!rv) {
+ mAdded = true;
+ }
+ return rv;
+}
+
+void
+AddFile::Finish(int status)
+{
+ LOG(("FINISH ADD " LOG_S, mRelPath.get()));
+ // Staged updates don't create backup files.
+ if (!sStagedUpdate) {
+ // When there is an update failure and a file has been added it is removed
+ // here since there might not be a backup to replace it.
+ if (status && mAdded) {
+ NS_tremove(mFile.get());
+ }
+ backup_finish(mFile.get(), mRelPath.get(), status);
+ }
+}
+
+class PatchFile : public Action
+{
+public:
+ PatchFile() : mPatchFile(nullptr), mPatchIndex(-1), buf(nullptr) { }
+
+ virtual ~PatchFile();
+
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare(); // should check for patch file and for checksum here
+ virtual int Execute();
+ virtual void Finish(int status);
+
+private:
+ int LoadSourceFile(FILE* ofile);
+
+ static int sPatchIndex;
+
+ const NS_tchar *mPatchFile;
+ mozilla::UniquePtr<NS_tchar[]> mFile;
+ mozilla::UniquePtr<NS_tchar[]> mFileRelPath;
+ int mPatchIndex;
+ MBSPatchHeader header;
+ unsigned char *buf;
+ NS_tchar spath[MAXPATHLEN];
+ AutoFile mPatchStream;
+};
+
+int PatchFile::sPatchIndex = 0;
+
+PatchFile::~PatchFile()
+{
+ // Make sure mPatchStream gets unlocked on Windows; the system will do that,
+ // but not until some indeterminate future time, and we want determinism.
+ // Normally this happens at the end of Execute, when we close the stream;
+ // this call is here in case Execute errors out.
+#ifdef XP_WIN
+ if (mPatchStream) {
+ UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1);
+ }
+#endif
+
+ // delete the temporary patch file
+ if (spath[0]) {
+ NS_tremove(spath);
+ }
+
+ if (buf) {
+ free(buf);
+ }
+}
+
+int
+PatchFile::LoadSourceFile(FILE* ofile)
+{
+ struct stat os;
+ int rv = fstat(fileno((FILE *)ofile), &os);
+ if (rv) {
+ LOG(("LoadSourceFile: unable to stat destination file: " LOG_S ", " \
+ "err: %d", mFileRelPath.get(), errno));
+ return READ_ERROR;
+ }
+
+ if (uint32_t(os.st_size) != header.slen) {
+ LOG(("LoadSourceFile: destination file size %d does not match expected size %d",
+ uint32_t(os.st_size), header.slen));
+ return LOADSOURCE_ERROR_WRONG_SIZE;
+ }
+
+ buf = (unsigned char *) malloc(header.slen);
+ if (!buf) {
+ return UPDATER_MEM_ERROR;
+ }
+
+ size_t r = header.slen;
+ unsigned char *rb = buf;
+ while (r) {
+ const size_t count = mmin(SSIZE_MAX, r);
+ size_t c = fread(rb, 1, count, ofile);
+ if (c != count) {
+ LOG(("LoadSourceFile: error reading destination file: " LOG_S,
+ mFileRelPath.get()));
+ return READ_ERROR;
+ }
+
+ r -= c;
+ rb += c;
+ }
+
+ // Verify that the contents of the source file correspond to what we expect.
+
+ unsigned int crc = crc32(buf, header.slen);
+
+ if (crc != header.scrc32) {
+ LOG(("LoadSourceFile: destination file crc %d does not match expected " \
+ "crc %d", crc, header.scrc32));
+ return CRC_ERROR;
+ }
+
+ return OK;
+}
+
+int
+PatchFile::Parse(NS_tchar *line)
+{
+ // format "<patchfile>" "<filetopatch>"
+
+ // Get the path to the patch file inside of the mar
+ mPatchFile = mstrtok(kQuote, &line);
+ if (!mPatchFile) {
+ return PARSE_ERROR;
+ }
+
+ // consume whitespace between args
+ NS_tchar *q = mstrtok(kQuote, &line);
+ if (!q) {
+ return PARSE_ERROR;
+ }
+
+ NS_tchar * validPath = get_valid_path(&line);
+ if (!validPath) {
+ return PARSE_ERROR;
+ }
+
+ mFileRelPath = mozilla::MakeUnique<NS_tchar[]>(MAXPATHLEN);
+ NS_tstrcpy(mFileRelPath.get(), validPath);
+
+ mFile.reset(get_full_path(validPath));
+ if (!mFile) {
+ return PARSE_ERROR;
+ }
+
+ return OK;
+}
+
+int
+PatchFile::Prepare()
+{
+ LOG(("PREPARE PATCH " LOG_S, mFileRelPath.get()));
+
+ // extract the patch to a temporary file
+ mPatchIndex = sPatchIndex++;
+
+ NS_tsnprintf(spath, sizeof(spath)/sizeof(spath[0]),
+ NS_T("%s/updating/%d.patch"), gWorkingDirPath, mPatchIndex);
+
+ NS_tremove(spath);
+
+ mPatchStream = NS_tfopen(spath, NS_T("wb+"));
+ if (!mPatchStream) {
+ return WRITE_ERROR;
+ }
+
+#ifdef XP_WIN
+ // Lock the patch file, so it can't be messed with between
+ // when we're done creating it and when we go to apply it.
+ if (!LockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1)) {
+ LOG(("Couldn't lock patch file: %d", GetLastError()));
+ return LOCK_ERROR_PATCH_FILE;
+ }
+
+ char sourcefile[MAXPATHLEN];
+ if (!WideCharToMultiByte(CP_UTF8, 0, mPatchFile, -1, sourcefile, MAXPATHLEN,
+ nullptr, nullptr)) {
+ LOG(("error converting wchar to utf8: %d", GetLastError()));
+ return STRING_CONVERSION_ERROR;
+ }
+
+ int rv = gArchiveReader.ExtractFileToStream(sourcefile, mPatchStream);
+#else
+ int rv = gArchiveReader.ExtractFileToStream(mPatchFile, mPatchStream);
+#endif
+
+ return rv;
+}
+
+int
+PatchFile::Execute()
+{
+ LOG(("EXECUTE PATCH " LOG_S, mFileRelPath.get()));
+
+ fseek(mPatchStream, 0, SEEK_SET);
+
+ int rv = MBS_ReadHeader(mPatchStream, &header);
+ if (rv) {
+ return rv;
+ }
+
+ FILE *origfile = nullptr;
+#ifdef XP_WIN
+ if (NS_tstrcmp(mFileRelPath.get(), gCallbackRelPath) == 0) {
+ // Read from the copy of the callback when patching since the callback can't
+ // be opened for reading to prevent the application from being launched.
+ origfile = NS_tfopen(gCallbackBackupPath, NS_T("rb"));
+ } else {
+ origfile = NS_tfopen(mFile.get(), NS_T("rb"));
+ }
+#else
+ origfile = NS_tfopen(mFile.get(), NS_T("rb"));
+#endif
+
+ if (!origfile) {
+ LOG(("unable to open destination file: " LOG_S ", err: %d",
+ mFileRelPath.get(), errno));
+ return READ_ERROR;
+ }
+
+ rv = LoadSourceFile(origfile);
+ fclose(origfile);
+ if (rv) {
+ LOG(("LoadSourceFile failed"));
+ return rv;
+ }
+
+ // Rename the destination file if it exists before proceeding so it can be
+ // used to restore the file to its original state if there is an error.
+ struct NS_tstat_t ss;
+ rv = NS_tstat(mFile.get(), &ss);
+ if (rv) {
+ LOG(("failed to read file status info: " LOG_S ", err: %d",
+ mFileRelPath.get(), errno));
+ return READ_ERROR;
+ }
+
+ // Staged updates don't need backup files.
+ if (!sStagedUpdate) {
+ rv = backup_create(mFile.get());
+ if (rv) {
+ return rv;
+ }
+ }
+
+#if defined(HAVE_POSIX_FALLOCATE)
+ AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode));
+ posix_fallocate(fileno((FILE *)ofile), 0, header.dlen);
+#elif defined(XP_WIN)
+ bool shouldTruncate = true;
+ // Creating the file, setting the size, and then closing the file handle
+ // lessens fragmentation more than any other method tested. Other methods that
+ // have been tested are:
+ // 1. _chsize / _chsize_s reduced fragmentation though not completely.
+ // 2. _get_osfhandle and then setting the size reduced fragmentation though
+ // not completely. There are also reports of _get_osfhandle failing on
+ // mingw.
+ HANDLE hfile = CreateFileW(mFile.get(),
+ GENERIC_WRITE,
+ 0,
+ nullptr,
+ CREATE_ALWAYS,
+ FILE_ATTRIBUTE_NORMAL,
+ nullptr);
+
+ if (hfile != INVALID_HANDLE_VALUE) {
+ if (SetFilePointer(hfile, header.dlen,
+ nullptr, FILE_BEGIN) != INVALID_SET_FILE_POINTER &&
+ SetEndOfFile(hfile) != 0) {
+ shouldTruncate = false;
+ }
+ CloseHandle(hfile);
+ }
+
+ AutoFile ofile(ensure_open(mFile.get(), shouldTruncate ? NS_T("wb+") : NS_T("rb+"),
+ ss.st_mode));
+#elif defined(XP_MACOSX)
+ AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode));
+ // Modified code from FileUtils.cpp
+ fstore_t store = {F_ALLOCATECONTIG, F_PEOFPOSMODE, 0, header.dlen};
+ // Try to get a continous chunk of disk space
+ rv = fcntl(fileno((FILE *)ofile), F_PREALLOCATE, &store);
+ if (rv == -1) {
+ // OK, perhaps we are too fragmented, allocate non-continuous
+ store.fst_flags = F_ALLOCATEALL;
+ rv = fcntl(fileno((FILE *)ofile), F_PREALLOCATE, &store);
+ }
+
+ if (rv != -1) {
+ ftruncate(fileno((FILE *)ofile), header.dlen);
+ }
+#else
+ AutoFile ofile(ensure_open(mFile.get(), NS_T("wb+"), ss.st_mode));
+#endif
+
+ if (ofile == nullptr) {
+ LOG(("unable to create new file: " LOG_S ", err: %d", mFileRelPath.get(),
+ errno));
+ return WRITE_ERROR_OPEN_PATCH_FILE;
+ }
+
+#ifdef XP_WIN
+ if (!shouldTruncate) {
+ fseek(ofile, 0, SEEK_SET);
+ }
+#endif
+
+ rv = MBS_ApplyPatch(&header, mPatchStream, buf, ofile);
+
+ // Go ahead and do a bit of cleanup now to minimize runtime overhead.
+ // Make sure mPatchStream gets unlocked on Windows; the system will do that,
+ // but not until some indeterminate future time, and we want determinism.
+#ifdef XP_WIN
+ UnlockFile((HANDLE)_get_osfhandle(fileno(mPatchStream)), 0, 0, -1, -1);
+#endif
+ // Set mPatchStream to nullptr to make AutoFile close the file,
+ // so it can be deleted on Windows.
+ mPatchStream = nullptr;
+ NS_tremove(spath);
+ spath[0] = NS_T('\0');
+ free(buf);
+ buf = nullptr;
+
+ return rv;
+}
+
+void
+PatchFile::Finish(int status)
+{
+ LOG(("FINISH PATCH " LOG_S, mFileRelPath.get()));
+
+ // Staged updates don't create backup files.
+ if (!sStagedUpdate) {
+ backup_finish(mFile.get(), mFileRelPath.get(), status);
+ }
+}
+
+class AddIfFile : public AddFile
+{
+public:
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare();
+ virtual int Execute();
+ virtual void Finish(int status);
+
+protected:
+ mozilla::UniquePtr<NS_tchar[]> mTestFile;
+};
+
+int
+AddIfFile::Parse(NS_tchar *line)
+{
+ // format "<testfile>" "<newfile>"
+
+ mTestFile.reset(get_full_path(get_valid_path(&line)));
+ if (!mTestFile) {
+ return PARSE_ERROR;
+ }
+
+ // consume whitespace between args
+ NS_tchar *q = mstrtok(kQuote, &line);
+ if (!q) {
+ return PARSE_ERROR;
+ }
+
+ return AddFile::Parse(line);
+}
+
+int
+AddIfFile::Prepare()
+{
+ // If the test file does not exist, then skip this action.
+ if (NS_taccess(mTestFile.get(), F_OK)) {
+ mTestFile = nullptr;
+ return OK;
+ }
+
+ return AddFile::Prepare();
+}
+
+int
+AddIfFile::Execute()
+{
+ if (!mTestFile)
+ return OK;
+
+ return AddFile::Execute();
+}
+
+void
+AddIfFile::Finish(int status)
+{
+ if (!mTestFile)
+ return;
+
+ AddFile::Finish(status);
+}
+
+class AddIfNotFile : public AddFile
+{
+public:
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare();
+ virtual int Execute();
+ virtual void Finish(int status);
+
+protected:
+ mozilla::UniquePtr<NS_tchar[]> mTestFile;
+};
+
+int
+AddIfNotFile::Parse(NS_tchar *line)
+{
+ // format "<testfile>" "<newfile>"
+
+ mTestFile.reset(get_full_path(get_valid_path(&line)));
+ if (!mTestFile) {
+ return PARSE_ERROR;
+ }
+
+ // consume whitespace between args
+ NS_tchar *q = mstrtok(kQuote, &line);
+ if (!q) {
+ return PARSE_ERROR;
+ }
+
+ return AddFile::Parse(line);
+}
+
+int
+AddIfNotFile::Prepare()
+{
+ // If the test file exists, then skip this action.
+ if (!NS_taccess(mTestFile.get(), F_OK)) {
+ mTestFile = NULL;
+ return OK;
+ }
+
+ return AddFile::Prepare();
+}
+
+int
+AddIfNotFile::Execute()
+{
+ if (!mTestFile)
+ return OK;
+
+ return AddFile::Execute();
+}
+
+void
+AddIfNotFile::Finish(int status)
+{
+ if (!mTestFile)
+ return;
+
+ AddFile::Finish(status);
+}
+
+class PatchIfFile : public PatchFile
+{
+public:
+ virtual int Parse(NS_tchar *line);
+ virtual int Prepare(); // should check for patch file and for checksum here
+ virtual int Execute();
+ virtual void Finish(int status);
+
+private:
+ mozilla::UniquePtr<NS_tchar[]> mTestFile;
+};
+
+int
+PatchIfFile::Parse(NS_tchar *line)
+{
+ // format "<testfile>" "<patchfile>" "<filetopatch>"
+
+ mTestFile.reset(get_full_path(get_valid_path(&line)));
+ if (!mTestFile) {
+ return PARSE_ERROR;
+ }
+
+ // consume whitespace between args
+ NS_tchar *q = mstrtok(kQuote, &line);
+ if (!q) {
+ return PARSE_ERROR;
+ }
+
+ return PatchFile::Parse(line);
+}
+
+int
+PatchIfFile::Prepare()
+{
+ // If the test file does not exist, then skip this action.
+ if (NS_taccess(mTestFile.get(), F_OK)) {
+ mTestFile = nullptr;
+ return OK;
+ }
+
+ return PatchFile::Prepare();
+}
+
+int
+PatchIfFile::Execute()
+{
+ if (!mTestFile)
+ return OK;
+
+ return PatchFile::Execute();
+}
+
+void
+PatchIfFile::Finish(int status)
+{
+ if (!mTestFile)
+ return;
+
+ PatchFile::Finish(status);
+}
+
+//-----------------------------------------------------------------------------
+
+#ifdef XP_WIN
+#include "nsWindowsRestart.cpp"
+#include "nsWindowsHelpers.h"
+#include "uachelper.h"
+#include "pathhash.h"
+
+/**
+ * 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.
+ *
+ * @param installationDir The path to the callback application binary.
+ * @param updateInfoDir The directory where update info is stored.
+ * @return true if there was no error starting the process.
+ */
+bool
+LaunchWinPostProcess(const WCHAR *installationDir,
+ const WCHAR *updateInfoDir)
+{
+ WCHAR workingDirectory[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(workingDirectory, installationDir, MAX_PATH);
+
+ // Launch helper.exe to perform post processing (e.g. registry and log file
+ // modifications) for the update.
+ WCHAR inifile[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(inifile, installationDir, MAX_PATH);
+ if (!PathAppendSafe(inifile, L"updater.ini")) {
+ return false;
+ }
+
+ WCHAR exefile[MAX_PATH + 1];
+ WCHAR exearg[MAX_PATH + 1];
+ WCHAR exeasync[10];
+ bool async = true;
+ if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeRelPath", nullptr,
+ exefile, MAX_PATH + 1, inifile)) {
+ return false;
+ }
+
+ if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeArg", nullptr, exearg,
+ MAX_PATH + 1, inifile)) {
+ return false;
+ }
+
+ if (!GetPrivateProfileStringW(L"PostUpdateWin", L"ExeAsync", L"TRUE",
+ exeasync,
+ sizeof(exeasync)/sizeof(exeasync[0]),
+ inifile)) {
+ return false;
+ }
+
+ // The relative path must not contain directory traversals, current directory,
+ // or colons.
+ if (wcsstr(exefile, L"..") != nullptr ||
+ wcsstr(exefile, L"./") != nullptr ||
+ wcsstr(exefile, L".\\") != nullptr ||
+ wcsstr(exefile, L":") != nullptr) {
+ return false;
+ }
+
+ // The relative path must not start with a decimal point, backslash, or
+ // forward slash.
+ if (exefile[0] == L'.' ||
+ exefile[0] == L'\\' ||
+ exefile[0] == L'/') {
+ return false;
+ }
+
+ WCHAR exefullpath[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(exefullpath, installationDir, MAX_PATH);
+ if (!PathAppendSafe(exefullpath, exefile)) {
+ return false;
+ }
+
+ if (!IsValidFullPath(exefullpath)) {
+ 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;
+ }
+
+ WCHAR slogFile[MAX_PATH + 1] = { L'\0' };
+ wcsncpy(slogFile, updateInfoDir, MAX_PATH);
+ if (!PathAppendSafe(slogFile, L"update.log")) {
+ return false;
+ }
+
+ WCHAR dummyArg[14] = { L'\0' };
+ wcsncpy(dummyArg, L"argv0ignored ", sizeof(dummyArg) / sizeof(dummyArg[0]) - 1);
+
+ size_t len = wcslen(exearg) + wcslen(dummyArg);
+ WCHAR *cmdline = (WCHAR *) malloc((len + 1) * sizeof(WCHAR));
+ if (!cmdline) {
+ return false;
+ }
+
+ 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.
+ CopyFileW(slogFile, dlogFile, false);
+
+ STARTUPINFOW si = {sizeof(si), 0};
+ si.lpDesktop = L"";
+ PROCESS_INFORMATION pi = {0};
+
+ bool ok = CreateProcessW(exefullpath,
+ cmdline,
+ nullptr, // no special security attributes
+ nullptr, // no special thread attributes
+ false, // don't inherit filehandles
+ 0, // No special process creation flags
+ nullptr, // inherit my environment
+ workingDirectory,
+ &si,
+ &pi);
+ free(cmdline);
+ if (ok) {
+ if (!async) {
+ WaitForSingleObject(pi.hProcess, INFINITE);
+ }
+ CloseHandle(pi.hProcess);
+ CloseHandle(pi.hThread);
+ }
+ return ok;
+}
+
+#endif
+
+static void
+LaunchCallbackApp(const NS_tchar *workingDir,
+ int argc,
+ NS_tchar **argv,
+ bool usingService)
+{
+ putenv(const_cast<char*>("NO_EM_RESTART="));
+ putenv(const_cast<char*>("MOZ_LAUNCHED_CHILD=1"));
+
+ // Run from the specified working directory (see bug 312360). This is not
+ // necessary on Windows CE since the application that launches the updater
+ // passes the working directory as an --environ: command line argument.
+ if (NS_tchdir(workingDir) != 0) {
+ LOG(("Warning: chdir failed"));
+ }
+
+#if defined(USE_EXECV)
+ execv(argv[0], argv);
+#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);
+ }
+#else
+# warning "Need implementaton of LaunchCallbackApp"
+#endif
+}
+
+static bool
+WriteStatusFile(const char* aStatus)
+{
+ NS_tchar filename[MAXPATHLEN] = {NS_T('\0')};
+#if defined(XP_WIN)
+ // The temp file is not removed on failure since there is client code that
+ // will remove it.
+ if (GetTempFileNameW(gPatchDirPath, L"sta", 0, filename) == 0) {
+ return false;
+ }
+#else
+ NS_tsnprintf(filename, sizeof(filename)/sizeof(filename[0]),
+ NS_T("%s/update.status"), gPatchDirPath);
+#endif
+
+ // Make sure that the directory for the update status file exists
+ if (ensure_parent_dir(filename)) {
+ return false;
+ }
+
+ // This is scoped to make the AutoFile close the file so it is possible to
+ // move the temp file to the update.status file on Windows.
+ {
+ AutoFile file(NS_tfopen(filename, NS_T("wb+")));
+ if (file == nullptr) {
+ return false;
+ }
+
+ if (fwrite(aStatus, strlen(aStatus), 1, file) != 1) {
+ return false;
+ }
+ }
+
+#if defined(XP_WIN)
+ NS_tchar dstfilename[MAXPATHLEN] = {NS_T('\0')};
+ NS_tsnprintf(dstfilename, sizeof(dstfilename)/sizeof(dstfilename[0]),
+ NS_T("%s\\update.status"), gPatchDirPath);
+ if (MoveFileExW(filename, dstfilename, MOVEFILE_REPLACE_EXISTING) == 0) {
+ return false;
+ }
+#endif
+
+ return true;
+}
+
+static void
+WriteStatusFile(int status)
+{
+ const char *text;
+
+ char buf[32];
+ if (status == OK) {
+ if (sStagedUpdate) {
+ text = "applied\n";
+ } else {
+ text = "succeeded\n";
+ }
+ } else {
+ snprintf(buf, sizeof(buf)/sizeof(buf[0]), "failed: %d\n", status);
+ text = buf;
+ }
+
+ 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
+ * the status is set to succeeded.
+ *
+ * @param isSucceeded Out parameter for specifying if the status
+ * is set to succeeded or not.
+ * @return true if the information was retrieved and it is succeeded.
+*/
+static bool
+IsUpdateStatusSucceeded(bool &isSucceeded)
+{
+ isSucceeded = false;
+ 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 kSucceeded[] = "succeeded";
+ isSucceeded = strncmp(buf, kSucceeded,
+ sizeof(kSucceeded) - 1) == 0;
+ return true;
+}
+#endif
+
+/*
+ * Copy the entire contents of the application installation directory to the
+ * destination directory for the update process.
+ *
+ * @return 0 if successful, an error code otherwise.
+ */
+static int
+CopyInstallDirToDestDir()
+{
+ // These files should not be copied over to the updated app
+#ifdef XP_WIN
+#define SKIPLIST_COUNT 3
+#elif XP_MACOSX
+#define SKIPLIST_COUNT 0
+#else
+#define SKIPLIST_COUNT 2
+#endif
+ copy_recursive_skiplist<SKIPLIST_COUNT> skiplist;
+#ifndef XP_MACOSX
+ skiplist.append(0, gInstallDirPath, NS_T("updated"));
+ skiplist.append(1, gInstallDirPath, NS_T("updates/0"));
+#ifdef XP_WIN
+ skiplist.append(2, gInstallDirPath, NS_T("updated.update_in_progress.lock"));
+#endif
+#endif
+
+ return ensure_copy_recursive(gInstallDirPath, gWorkingDirPath, skiplist);
+}
+
+/*
+ * Replace the application installation directory with the destination
+ * directory in order to finish a staged update task
+ *
+ * @return 0 if successful, an error code otherwise.
+ */
+static int
+ProcessReplaceRequest()
+{
+ // The replacement algorithm is like this:
+ // 1. Move destDir to tmpDir. In case of failure, abort.
+ // 2. Move newDir to destDir. In case of failure, revert step 1 and abort.
+ // 3. Delete tmpDir (or defer it to the next reboot).
+
+#ifdef XP_MACOSX
+ NS_tchar destDir[MAXPATHLEN];
+ NS_tsnprintf(destDir, sizeof(destDir)/sizeof(destDir[0]),
+ NS_T("%s/Contents"), gInstallDirPath);
+#elif XP_WIN
+ // Windows preserves the case of the file/directory names. We use the
+ // GetLongPathName API in order to get the correct case for the directory
+ // name, so that if the user has used a different case when launching the
+ // application, the installation directory's name does not change.
+ NS_tchar destDir[MAXPATHLEN];
+ if (!GetLongPathNameW(gInstallDirPath, destDir,
+ sizeof(destDir)/sizeof(destDir[0]))) {
+ return NO_INSTALLDIR_ERROR;
+ }
+#else
+ NS_tchar* destDir = gInstallDirPath;
+#endif
+
+ NS_tchar tmpDir[MAXPATHLEN];
+ NS_tsnprintf(tmpDir, sizeof(tmpDir)/sizeof(tmpDir[0]),
+ NS_T("%s.bak"), destDir);
+
+ NS_tchar newDir[MAXPATHLEN];
+ NS_tsnprintf(newDir, sizeof(newDir)/sizeof(newDir[0]),
+#ifdef XP_MACOSX
+ NS_T("%s/Contents"),
+ gWorkingDirPath);
+#else
+ NS_T("%s.bak/updated"),
+ gInstallDirPath);
+#endif
+
+ // First try to remove the possibly existing temp directory, because if this
+ // directory exists, we will fail to rename destDir.
+ // No need to error check here because if this fails, we will fail in the
+ // next step anyways.
+ ensure_remove_recursive(tmpDir);
+
+ LOG(("Begin moving destDir (" LOG_S ") to tmpDir (" LOG_S ")",
+ destDir, tmpDir));
+ int rv = rename_file(destDir, tmpDir, true);
+#ifdef XP_WIN
+ // On Windows, if Firefox is launched using the shortcut, it will hold a handle
+ // to its installation directory open, which might not get released in time.
+ // Therefore we wait a little bit here to see if the handle is released.
+ // If it's not released, we just fail to perform the replace request.
+ const int max_retries = 10;
+ int retries = 0;
+ while (rv == WRITE_ERROR && (retries++ < max_retries)) {
+ LOG(("PerformReplaceRequest: destDir rename attempt %d failed. " \
+ "File: " LOG_S ". Last error: %d, err: %d", retries,
+ destDir, GetLastError(), rv));
+
+ Sleep(100);
+
+ rv = rename_file(destDir, tmpDir, true);
+ }
+#endif
+ if (rv) {
+ // The status file will have 'pending' written to it so there is no value in
+ // returning an error specific for this failure.
+ LOG(("Moving destDir to tmpDir failed, err: %d", rv));
+ return rv;
+ }
+
+ LOG(("Begin moving newDir (" LOG_S ") to destDir (" LOG_S ")",
+ newDir, destDir));
+ rv = rename_file(newDir, destDir, true);
+#ifdef XP_MACOSX
+ if (rv) {
+ LOG(("Moving failed. Begin copying newDir (" LOG_S ") to destDir (" LOG_S ")",
+ newDir, destDir));
+ copy_recursive_skiplist<0> skiplist;
+ rv = ensure_copy_recursive(newDir, destDir, skiplist);
+ }
+#endif
+ if (rv) {
+ LOG(("Moving newDir to destDir failed, err: %d", rv));
+ LOG(("Now, try to move tmpDir back to destDir"));
+ ensure_remove_recursive(destDir);
+ int rv2 = rename_file(tmpDir, destDir, true);
+ if (rv2) {
+ LOG(("Moving tmpDir back to destDir failed, err: %d", rv2));
+ }
+ // The status file will be have 'pending' written to it so there is no value
+ // in returning an error specific for this failure.
+ return rv;
+ }
+
+#if !defined(XP_WIN) && !defined(XP_MACOSX)
+ // Platforms that have their updates directory in the installation directory
+ // need to have the last-update.log and backup-update.log files moved from the
+ // old installation directory to the new installation directory.
+ NS_tchar tmpLog[MAXPATHLEN];
+ NS_tsnprintf(tmpLog, sizeof(tmpLog)/sizeof(tmpLog[0]),
+ NS_T("%s/updates/last-update.log"), tmpDir);
+ if (!NS_taccess(tmpLog, F_OK)) {
+ NS_tchar destLog[MAXPATHLEN];
+ NS_tsnprintf(destLog, sizeof(destLog)/sizeof(destLog[0]),
+ NS_T("%s/updates/last-update.log"), destDir);
+ NS_tremove(destLog);
+ NS_trename(tmpLog, destLog);
+ }
+#endif
+
+ LOG(("Now, remove the tmpDir"));
+ rv = ensure_remove_recursive(tmpDir, true);
+ if (rv) {
+ LOG(("Removing tmpDir failed, err: %d", rv));
+#ifdef XP_WIN
+ NS_tchar deleteDir[MAXPATHLEN];
+ NS_tsnprintf(deleteDir, sizeof(deleteDir)/sizeof(deleteDir[0]),
+ NS_T("%s\\%s"), destDir, DELETE_DIR);
+ // Attempt to remove the tobedeleted directory and then recreate it if it
+ // was successfully removed.
+ _wrmdir(deleteDir);
+ if (NS_taccess(deleteDir, F_OK)) {
+ NS_tmkdir(deleteDir, 0755);
+ }
+ remove_recursive_on_reboot(tmpDir, deleteDir);
+#endif
+ }
+
+#ifdef XP_MACOSX
+ // On OS X, we we need to remove the staging directory after its Contents
+ // directory has been moved.
+ NS_tchar updatedAppDir[MAXPATHLEN];
+ NS_tsnprintf(updatedAppDir, sizeof(updatedAppDir)/sizeof(updatedAppDir[0]),
+ NS_T("%s/Updated.app"), gPatchDirPath);
+ ensure_remove_recursive(updatedAppDir);
+#endif
+
+ gSucceeded = true;
+
+ return 0;
+}
+
+#ifdef XP_WIN
+static void
+WaitForServiceFinishThread(void *param)
+{
+ // We wait at most 10 minutes, we already waited 5 seconds previously
+ // before deciding to show this UI.
+ WaitForServiceStop(SVC_NAME, 595);
+ QuitProgressUI();
+}
+#endif
+
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+/**
+ * This function reads in the ACCEPTED_MAR_CHANNEL_IDS from update-settings.ini
+ *
+ * @param path The path to the ini file that is to be read
+ * @param results A pointer to the location to store the read strings
+ * @return OK on success
+ */
+static int
+ReadMARChannelIDs(const NS_tchar *path, MARChannelStringTable *results)
+{
+ const unsigned int kNumStrings = 1;
+ const char *kUpdaterKeys = "ACCEPTED_MAR_CHANNEL_IDS\0";
+ char updater_strings[kNumStrings][MAX_TEXT_LEN];
+
+ int result = ReadStrings(path, kUpdaterKeys, kNumStrings,
+ updater_strings, "Settings");
+
+ strncpy(results->MARChannelID, updater_strings[0], MAX_TEXT_LEN - 1);
+ results->MARChannelID[MAX_TEXT_LEN - 1] = 0;
+
+ return result;
+}
+#endif
+
+static int
+GetUpdateFileName(NS_tchar *fileName, int maxChars)
+{
+#if defined(MOZ_WIDGET_GONK)
+ // If an update.link file exists, then it will contain the name
+ // of the update file (terminated by a newline).
+
+ NS_tchar linkFileName[MAXPATHLEN];
+ NS_tsnprintf(linkFileName, sizeof(linkFileName)/sizeof(linkFileName[0]),
+ NS_T("%s/update.link"), gPatchDirPath);
+ AutoFile linkFile(NS_tfopen(linkFileName, NS_T("rb")));
+ if (linkFile == nullptr) {
+ NS_tsnprintf(fileName, maxChars,
+ NS_T("%s/update.mar"), gPatchDirPath);
+ return OK;
+ }
+
+ char dataFileName[MAXPATHLEN];
+ size_t bytesRead;
+
+ if ((bytesRead = fread(dataFileName, 1, sizeof(dataFileName)-1, linkFile)) <= 0) {
+ *fileName = NS_T('\0');
+ return READ_ERROR;
+ }
+ if (dataFileName[bytesRead-1] == '\n') {
+ // Strip trailing newline (for \n and \r\n)
+ bytesRead--;
+ }
+ if (dataFileName[bytesRead-1] == '\r') {
+ // Strip trailing CR (for \r, \r\n)
+ bytesRead--;
+ }
+ dataFileName[bytesRead] = '\0';
+
+ strncpy(fileName, dataFileName, maxChars-1);
+ fileName[maxChars-1] = '\0';
+#else
+ // We currently only support update.link files under GONK
+ NS_tsnprintf(fileName, maxChars,
+ NS_T("%s/update.mar"), gPatchDirPath);
+#endif
+ return OK;
+}
+
+static void
+UpdateThreadFunc(void *param)
+{
+ // open ZIP archive and process...
+ int rv;
+ if (sReplaceRequest) {
+ rv = ProcessReplaceRequest();
+ } else {
+ NS_tchar dataFile[MAXPATHLEN];
+ rv = GetUpdateFileName(dataFile, sizeof(dataFile)/sizeof(dataFile[0]));
+ if (rv == OK) {
+ rv = gArchiveReader.Open(dataFile);
+ }
+
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+ if (rv == OK) {
+#ifdef XP_WIN
+ HKEY baseKey = nullptr;
+ wchar_t valueName[] = L"Image Path";
+ wchar_t rasenh[] = L"rsaenh.dll";
+ bool reset = false;
+ if (RegOpenKeyExW(HKEY_LOCAL_MACHINE,
+ L"SOFTWARE\\Microsoft\\Cryptography\\Defaults\\Provider\\Microsoft Enhanced Cryptographic Provider v1.0",
+ 0, KEY_READ | KEY_WRITE,
+ &baseKey) == ERROR_SUCCESS) {
+ wchar_t path[MAX_PATH + 1];
+ DWORD size = sizeof(path);
+ DWORD type;
+ if (RegQueryValueExW(baseKey, valueName, 0, &type,
+ (LPBYTE)path, &size) == ERROR_SUCCESS) {
+ if (type == REG_SZ && wcscmp(path, rasenh) == 0) {
+ wchar_t rasenhFullPath[] = L"%SystemRoot%\\System32\\rsaenh.dll";
+ if (RegSetValueExW(baseKey, valueName, 0, REG_SZ,
+ (const BYTE*)rasenhFullPath,
+ sizeof(rasenhFullPath)) == ERROR_SUCCESS) {
+ reset = true;
+ }
+ }
+ }
+ }
+#endif
+ rv = gArchiveReader.VerifySignature();
+#ifdef XP_WIN
+ if (baseKey) {
+ if (reset) {
+ RegSetValueExW(baseKey, valueName, 0, REG_SZ,
+ (const BYTE*)rasenh,
+ sizeof(rasenh));
+ }
+ RegCloseKey(baseKey);
+ }
+#endif
+ }
+
+ if (rv == OK) {
+ if (rv == OK) {
+ NS_tchar updateSettingsPath[MAX_TEXT_LEN];
+ NS_tsnprintf(updateSettingsPath,
+ sizeof(updateSettingsPath) / sizeof(updateSettingsPath[0]),
+#ifdef XP_MACOSX
+ NS_T("%s/Contents/Resources/update-settings.ini"),
+#else
+ NS_T("%s/update-settings.ini"),
+#endif
+ gWorkingDirPath);
+ MARChannelStringTable MARStrings;
+ if (ReadMARChannelIDs(updateSettingsPath, &MARStrings) != OK) {
+ // If we can't read from update-settings.ini then we shouldn't impose
+ // a MAR restriction. Some installations won't even include this file.
+ MARStrings.MARChannelID[0] = '\0';
+ }
+
+ rv = gArchiveReader.VerifyProductInformation(MARStrings.MARChannelID,
+ MOZ_APP_VERSION);
+ }
+ }
+#endif
+
+ if (rv == OK && sStagedUpdate && !sIsOSUpdate) {
+#ifdef TEST_UPDATER
+ // The MOZ_TEST_SKIP_UPDATE_STAGE environment variable prevents copying
+ // the files in dist/bin in the test updater when staging an update since
+ // this can cause tests to timeout.
+ if (EnvHasValue("MOZ_TEST_SKIP_UPDATE_STAGE")) {
+ rv = OK;
+ } else {
+ rv = CopyInstallDirToDestDir();
+ }
+#else
+ rv = CopyInstallDirToDestDir();
+#endif
+ }
+
+ if (rv == OK) {
+ rv = DoUpdate();
+ gArchiveReader.Close();
+ NS_tchar updatingDir[MAXPATHLEN];
+ NS_tsnprintf(updatingDir, sizeof(updatingDir)/sizeof(updatingDir[0]),
+ NS_T("%s/updating"), gWorkingDirPath);
+ ensure_remove_recursive(updatingDir);
+ }
+ }
+
+ if (rv && (sReplaceRequest || sStagedUpdate)) {
+#ifdef XP_WIN
+ // On Windows, the current working directory of the process should be changed
+ // so that it's not locked.
+ if (sStagedUpdate) {
+ NS_tchar sysDir[MAX_PATH + 1] = { L'\0' };
+ if (GetSystemDirectoryW(sysDir, MAX_PATH + 1)) {
+ NS_tchdir(sysDir);
+ }
+ }
+#endif
+ ensure_remove_recursive(gWorkingDirPath);
+ // When attempting to replace the application, we should fall back
+ // to non-staged updates in case of a failure. We do this by
+ // setting the status to pending, exiting the updater, and
+ // launching the callback application. The callback application's
+ // startup path will see the pending status, and will start the
+ // updater application again in order to apply the update without
+ // staging.
+ if (sReplaceRequest) {
+ WriteStatusFile(sUsingService ? "pending-service" : "pending");
+ } else {
+ WriteStatusFile(rv);
+ }
+#ifdef TEST_UPDATER
+ // Some tests need to use --test-process-updates again.
+ putenv(const_cast<char*>("MOZ_TEST_PROCESS_UPDATES="));
+#endif
+ } else {
+ if (rv) {
+ LOG(("failed: %d", rv));
+ } else {
+#ifdef XP_MACOSX
+ // If the update was successful we need to update the timestamp on the
+ // top-level Mac OS X bundle directory so that Mac OS X's Launch Services
+ // picks up any major changes when the bundle is updated.
+ if (!sStagedUpdate && utimes(gInstallDirPath, nullptr) != 0) {
+ LOG(("Couldn't set access/modification time on application bundle."));
+ }
+#endif
+
+ LOG(("succeeded"));
+ }
+ WriteStatusFile(rv);
+ }
+
+ LOG(("calling QuitProgressUI"));
+ QuitProgressUI();
+}
+
+#ifdef XP_MACOSX
+static void
+ServeElevatedUpdateThreadFunc(void* param)
+{
+ UpdateServerThreadArgs* threadArgs = (UpdateServerThreadArgs*)param;
+ gSucceeded = ServeElevatedUpdate(threadArgs->argc, threadArgs->argv);
+ if (!gSucceeded) {
+ WriteStatusFile(ELEVATION_CANCELED);
+ }
+ QuitProgressUI();
+}
+
+void freeArguments(int argc, char** argv)
+{
+ for (int i = 0; i < argc; i++) {
+ free(argv[i]);
+ }
+ free(argv);
+}
+#endif
+
+int LaunchCallbackAndPostProcessApps(int argc, NS_tchar** argv,
+ int callbackIndex
+#ifdef XP_WIN
+ , const WCHAR* elevatedLockFilePath
+ , HANDLE updateLockFileHandle
+#elif XP_MACOSX
+ , bool isElevated
+#endif
+ )
+{
+ if (argc > callbackIndex) {
+#if defined(XP_WIN)
+ if (gSucceeded) {
+ 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
+ if (!isElevated) {
+ if (gSucceeded) {
+ LaunchMacPostProcess(gInstallDirPath);
+ }
+#endif
+
+ LaunchCallbackApp(argv[5],
+ argc - callbackIndex,
+ argv + callbackIndex,
+ sUsingService);
+#ifdef XP_MACOSX
+ } // if (!isElevated)
+#endif /* XP_MACOSX */
+ }
+ return 0;
+}
+
+int NS_main(int argc, NS_tchar **argv)
+{
+ // The callback is the remaining arguments starting at callbackIndex.
+ // The argument specified by callbackIndex is the callback executable and the
+ // argument prior to callbackIndex is the working directory.
+ const int callbackIndex = 6;
+
+#ifdef XP_MACOSX
+ bool isElevated =
+ strstr(argv[0], "/Library/PrivilegedHelperTools/org.mozilla.updater") != 0;
+ if (isElevated) {
+ if (!ObtainUpdaterArguments(&argc, &argv)) {
+ // Won't actually get here because ObtainUpdaterArguments will terminate
+ // the current process on failure.
+ return 1;
+ }
+ }
+#endif
+
+#if defined(MOZ_WIDGET_GONK)
+ if (EnvHasValue("LD_PRELOAD")) {
+ // If the updater is launched with LD_PRELOAD set, then we wind up
+ // preloading libmozglue.so. Under some circumstances, this can cause
+ // the remount of /system to fail when going from rw to ro, so if we
+ // detect LD_PRELOAD we unsetenv it and relaunch ourselves without it.
+ // This will cause the offending preloaded library to be closed.
+ //
+ // For a variety of reasons, this is really hard to do in a safe manner
+ // in the parent process, so we do it here.
+ unsetenv("LD_PRELOAD");
+ execv(argv[0], argv);
+ __android_log_print(ANDROID_LOG_INFO, "updater",
+ "execve failed: errno: %d. Exiting...", errno);
+ _exit(1);
+ }
+#endif
+
+#if defined(MOZ_VERIFY_MAR_SIGNATURE) && !defined(XP_WIN) && \
+ !defined(XP_MACOSX) && !defined(MOZ_WIDGET_GONK)
+ // On Windows and Mac we rely on native APIs to do verifications so we don't
+ // need to initialize NSS at all there.
+ // Otherwise, minimize the amount of NSS we depend on by avoiding all the NSS
+ // databases.
+ if (NSS_NoDB_Init(NULL) != SECSuccess) {
+ PRErrorCode error = PR_GetError();
+ fprintf(stderr, "Could not initialize NSS: %s (%d)",
+ PR_ErrorToName(error), (int) error);
+ _exit(1);
+ }
+#endif
+
+#ifdef XP_MACOSX
+ if (!isElevated) {
+#endif
+ InitProgressUI(&argc, &argv);
+#ifdef XP_MACOSX
+ }
+#endif
+
+ // To process an update the updater command line must at a minimum have the
+ // directory path containing the updater.mar file to process as the first
+ // argument, the install directory as the second argument, and the directory
+ // to apply the update to as the third argument. When the updater is launched
+ // by another process the PID of the parent process should be provided in the
+ // optional fourth argument and the updater will wait on the parent process to
+ // exit if the value is non-zero and the process is present. This is necessary
+ // due to not being able to update files that are in use on Windows. The
+ // optional fifth argument is the callback's working directory and the
+ // optional sixth argument is the callback path. The callback is the
+ // application to launch after updating and it will be launched when these
+ // arguments are provided whether the update was successful or not. All
+ // remaining arguments are optional and are passed to the callback when it is
+ // launched.
+ if (argc < 4) {
+ fprintf(stderr, "Usage: updater patch-dir install-dir apply-to-dir [wait-pid [callback-working-dir callback-path args...]]\n");
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ 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.
+ fprintf(stderr, "The patch directory path is not valid for this " \
+ "application (" LOG_S ")\n", argv[1]);
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+ // 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 " \
+ "application (" LOG_S ")\n", argv[2]);
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+ // The directory we're going to update to.
+ // We copy this string because we need to remove trailing slashes. The C++
+ // standard says that it's always safe to write to strings pointed to by argv
+ // elements, but I don't necessarily believe it.
+ NS_tstrncpy(gInstallDirPath, argv[2], MAXPATHLEN);
+ gInstallDirPath[MAXPATHLEN - 1] = NS_T('\0');
+ NS_tchar *slash = NS_tstrrchr(gInstallDirPath, NS_SLASH);
+ if (slash && !slash[1]) {
+ *slash = NS_T('\0');
+ }
+
+#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
+ {
+ HKEY hkApp = nullptr;
+ RegCreateKeyExW(HKEY_CURRENT_USER, L"Software\\Classes\\Applications",
+ 0, nullptr, REG_OPTION_NON_VOLATILE, KEY_SET_VALUE, nullptr,
+ &hkApp, nullptr);
+ RegCloseKey(hkApp);
+ if (RegCreateKeyExW(HKEY_CURRENT_USER,
+ L"Software\\Classes\\Applications\\updater.exe",
+ 0, nullptr, REG_OPTION_VOLATILE, KEY_SET_VALUE, nullptr,
+ &hkApp, nullptr) == ERROR_SUCCESS) {
+ RegSetValueExW(hkApp, L"IsHostApp", 0, REG_NONE, 0, 0);
+ RegSetValueExW(hkApp, L"NoOpenWith", 0, REG_NONE, 0, 0);
+ RegSetValueExW(hkApp, L"NoStartPage", 0, REG_NONE, 0, 0);
+ RegCloseKey(hkApp);
+ }
+ }
+#endif
+
+ // If there is a PID specified and it is not '0' then wait for the process to exit.
+#ifdef XP_WIN
+ __int64 pid = 0;
+#else
+ int pid = 0;
+#endif
+ if (argc > 4) {
+#ifdef XP_WIN
+ pid = _wtoi64(argv[4]);
+#else
+ pid = atoi(argv[4]);
+#endif
+ if (pid == -1) {
+ // This is a signal from the parent process that the updater should stage
+ // the update.
+ sStagedUpdate = true;
+ } else if (NS_tstrstr(argv[4], NS_T("/replace"))) {
+ // We're processing a request to replace the application with a staged
+ // update.
+ sReplaceRequest = true;
+ }
+ }
+
+ // 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 " \
+ "application (" LOG_S ")\n", argv[3]);
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+ // The directory we're going to update to.
+ // We copy this string because we need to remove trailing slashes. The C++
+ // standard says that it's always safe to write to strings pointed to by argv
+ // elements, but I don't necessarily believe it.
+ NS_tstrncpy(gWorkingDirPath, argv[3], MAXPATHLEN);
+ gWorkingDirPath[MAXPATHLEN - 1] = NS_T('\0');
+ slash = NS_tstrrchr(gWorkingDirPath, NS_SLASH);
+ if (slash && !slash[1]) {
+ *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);
+ fprintf(stderr, "The callback file path is not valid for this " \
+ "application (" LOG_S ")\n", argv[callbackIndex]);
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+
+ size_t len = NS_tstrlen(gInstallDirPath);
+ NS_tchar callbackInstallDir[MAXPATHLEN] = { NS_T('\0') };
+ NS_tstrncpy(callbackInstallDir, argv[callbackIndex], len);
+ if (NS_tstrcmp(gInstallDirPath, callbackInstallDir) != 0) {
+ WriteStatusFile(INVALID_CALLBACK_DIR_ERROR);
+ fprintf(stderr, "The callback file must be located in the " \
+ "installation directory (" LOG_S ")\n", argv[callbackIndex]);
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+ }
+
+#ifdef XP_MACOSX
+ if (!isElevated && !IsRecursivelyWritable(argv[2])) {
+ // If the app directory isn't recursively writeable, an elevated update is
+ // required.
+ UpdateServerThreadArgs threadArgs;
+ threadArgs.argc = argc;
+ threadArgs.argv = const_cast<const NS_tchar**>(argv);
+
+ Thread t1;
+ if (t1.Run(ServeElevatedUpdateThreadFunc, &threadArgs) == 0) {
+ // Show an indeterminate progress bar while an elevated update is in
+ // progress.
+ ShowProgressUI(true);
+ }
+ t1.Join();
+
+ LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex, false);
+ return gSucceeded ? 0 : 1;
+ }
+#endif
+
+ if (EnvHasValue("MOZ_OS_UPDATE")) {
+ sIsOSUpdate = true;
+ putenv(const_cast<char*>("MOZ_OS_UPDATE="));
+ }
+
+ LogInit(gPatchDirPath, NS_T("update.log"));
+
+ if (!WriteStatusFile("applying")) {
+ LOG(("failed setting status to 'applying'"));
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+
+ if (sStagedUpdate) {
+ LOG(("Performing a staged update"));
+ } else if (sReplaceRequest) {
+ LOG(("Performing a replace request"));
+ }
+
+ LOG(("PATCH DIRECTORY " LOG_S, gPatchDirPath));
+ LOG(("INSTALLATION DIRECTORY " LOG_S, gInstallDirPath));
+ 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);
+ LOG(("Installation directory and working directory must be the same "
+ "for non-staged updates. Exiting."));
+ LogFinish();
+ return 1;
+ }
+
+ NS_tchar workingDirParent[MAX_PATH];
+ NS_tsnprintf(workingDirParent,
+ sizeof(workingDirParent) / sizeof(workingDirParent[0]),
+ NS_T("%s"), gWorkingDirPath);
+ if (!PathRemoveFileSpecW(workingDirParent)) {
+ WriteStatusFile(REMOVE_FILE_SPEC_ERROR);
+ LOG(("Error calling PathRemoveFileSpecW: %d", GetLastError()));
+ LogFinish();
+ return 1;
+ }
+
+ if (_wcsnicmp(workingDirParent, gInstallDirPath, MAX_PATH) != 0) {
+ WriteStatusFile(INVALID_APPLYTO_DIR_STAGED_ERROR);
+ LOG(("The apply-to directory must be the same as or "
+ "a child of the installation directory! Exiting."));
+ LogFinish();
+ return 1;
+ }
+ }
+#endif
+
+#ifdef MOZ_WIDGET_GONK
+ const char *prioEnv = getenv("MOZ_UPDATER_PRIO");
+ if (prioEnv) {
+ int32_t prioVal;
+ int32_t oomScoreAdj;
+ int32_t ioprioClass;
+ int32_t ioprioLevel;
+ if (sscanf(prioEnv, "%d/%d/%d/%d",
+ &prioVal, &oomScoreAdj, &ioprioClass, &ioprioLevel) == 4) {
+ LOG(("MOZ_UPDATER_PRIO=%s", prioEnv));
+ if (setpriority(PRIO_PROCESS, 0, prioVal)) {
+ LOG(("setpriority(%d) failed, errno = %d", prioVal, errno));
+ }
+ if (ioprio_set(IOPRIO_WHO_PROCESS, 0,
+ IOPRIO_PRIO_VALUE(ioprioClass, ioprioLevel))) {
+ LOG(("ioprio_set(%d,%d) failed: errno = %d",
+ ioprioClass, ioprioLevel, errno));
+ }
+ FILE *fs = fopen("/proc/self/oom_score_adj", "w");
+ if (fs) {
+ fprintf(fs, "%d", oomScoreAdj);
+ fclose(fs);
+ } else {
+ LOG(("Unable to open /proc/self/oom_score_adj for writing, errno = %d",
+ errno));
+ }
+ }
+ }
+#endif
+
+#ifdef XP_WIN
+ if (pid > 0) {
+ HANDLE parent = OpenProcess(SYNCHRONIZE, false, (DWORD) pid);
+ // May return nullptr if the parent process has already gone away.
+ // Otherwise, wait for the parent process to exit before starting the
+ // update.
+ if (parent) {
+ DWORD waitTime = PARENT_WAIT;
+ DWORD result = WaitForSingleObject(parent, waitTime);
+ CloseHandle(parent);
+ if (result != WAIT_OBJECT_0) {
+ return 1;
+ }
+ }
+ }
+#else
+ if (pid > 0)
+ waitpid(pid, nullptr, 0);
+#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)) {
+ NS_tchar updateLockFilePath[MAXPATHLEN];
+ if (sStagedUpdate) {
+ // When staging an update, the lock file is:
+ // <install_dir>\updated.update_in_progress.lock
+ NS_tsnprintf(updateLockFilePath,
+ sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]),
+ NS_T("%s/updated.update_in_progress.lock"), gInstallDirPath);
+ } else if (sReplaceRequest) {
+ // When processing a replace request, the lock file is:
+ // <install_dir>\..\moz_update_in_progress.lock
+ NS_tchar installDir[MAXPATHLEN];
+ NS_tstrcpy(installDir, gInstallDirPath);
+ NS_tchar *slash = (NS_tchar *) NS_tstrrchr(installDir, NS_SLASH);
+ *slash = NS_T('\0');
+ NS_tsnprintf(updateLockFilePath,
+ sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]),
+ NS_T("%s\\moz_update_in_progress.lock"), installDir);
+ } else {
+ // In the non-staging update case, the lock file is:
+ // <install_dir>\<app_name>.exe.update_in_progress.lock
+ NS_tsnprintf(updateLockFilePath,
+ sizeof(updateLockFilePath)/sizeof(updateLockFilePath[0]),
+ NS_T("%s.update_in_progress.lock"), argv[callbackIndex]);
+ }
+
+ // The update_in_progress.lock file should only exist during an update. In
+ // case it exists attempt to remove it and exit if that fails to prevent
+ // simultaneous updates occurring.
+ if (!_waccess(updateLockFilePath, F_OK) &&
+ NS_tremove(updateLockFilePath) != 0) {
+ // Try to fall back to the old way of doing updates if a staged
+ // update fails.
+ if (sStagedUpdate || sReplaceRequest) {
+ // Note that this could fail, but if it does, there isn't too much we
+ // can do in order to recover anyways.
+ WriteStatusFile("pending");
+ }
+ LOG(("Update already in progress! Exiting"));
+ return 1;
+ }
+
+ updateLockFileHandle = CreateFileW(updateLockFilePath,
+ GENERIC_READ | GENERIC_WRITE,
+ 0,
+ nullptr,
+ OPEN_ALWAYS,
+ FILE_FLAG_DELETE_ON_CLOSE,
+ nullptr);
+
+ NS_tsnprintf(elevatedLockFilePath,
+ sizeof(elevatedLockFilePath)/sizeof(elevatedLockFilePath[0]),
+ NS_T("%s/update_elevated.lock"), gPatchDirPath);
+
+ // Even if a file has no sharing access, you can still get its attributes
+ 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
+ // 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
+ // to start the callback application.
+ if (startedFromUnelevatedUpdater) {
+ // Disable every privilege we don't need. Processes started using
+ // CreateProcess will use the same token as this process.
+ UACHelper::DisablePrivileges(nullptr);
+ }
+
+ if (updateLockFileHandle == INVALID_HANDLE_VALUE ||
+ (useService && testOnlyFallbackKeyExists && noServiceFallback)) {
+ if (!_waccess(elevatedLockFilePath, F_OK) &&
+ NS_tremove(elevatedLockFilePath) != 0) {
+ fprintf(stderr, "Unable to create elevated lock file! Exiting\n");
+ return 1;
+ }
+
+ HANDLE elevatedFileHandle;
+ elevatedFileHandle = CreateFileW(elevatedLockFilePath,
+ GENERIC_READ | GENERIC_WRITE,
+ 0,
+ nullptr,
+ OPEN_ALWAYS,
+ FILE_FLAG_DELETE_ON_CLOSE,
+ nullptr);
+
+ if (elevatedFileHandle == INVALID_HANDLE_VALUE) {
+ LOG(("Unable to create elevated lock file! Exiting"));
+ return 1;
+ }
+
+ wchar_t *cmdLine = MakeCommandLine(argc - 1, argv + 1);
+ if (!cmdLine) {
+ CloseHandle(elevatedFileHandle);
+ 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
+ // 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 (updateLockFileHandle != INVALID_HANDLE_VALUE) {
+ CloseHandle(updateLockFileHandle);
+ }
+ WriteStatusFile("pending");
+ 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.
+ // 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) {
+ SHELLEXECUTEINFO sinfo;
+ memset(&sinfo, 0, sizeof(SHELLEXECUTEINFO));
+ sinfo.cbSize = sizeof(SHELLEXECUTEINFO);
+ sinfo.fMask = SEE_MASK_FLAG_NO_UI |
+ SEE_MASK_FLAG_DDEWAIT |
+ SEE_MASK_NOCLOSEPROCESS;
+ sinfo.hwnd = nullptr;
+ sinfo.lpFile = argv[0];
+ sinfo.lpParameters = cmdLine;
+ sinfo.lpVerb = L"runas";
+ sinfo.nShow = SW_SHOWNORMAL;
+
+ bool result = ShellExecuteEx(&sinfo);
+ free(cmdLine);
+
+ if (result) {
+ WaitForSingleObject(sinfo.hProcess, INFINITE);
+ CloseHandle(sinfo.hProcess);
+ } else {
+ WriteStatusFile(ELEVATION_CANCELED);
+ }
+ }
+
+ if (argc > callbackIndex) {
+ LaunchCallbackApp(argv[5], argc - callbackIndex,
+ argv + callbackIndex, sUsingService);
+ }
+
+ CloseHandle(elevatedFileHandle);
+
+ if (!useService && !noServiceFallback &&
+ INVALID_HANDLE_VALUE == updateLockFileHandle) {
+ // We didn't use the service and we did run 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.
+ CloseHandle(updateLockFileHandle);
+ WriteStatusFile(lastFallbackError);
+ return 0;
+ }
+ }
+ }
+#endif
+
+#if defined(MOZ_WIDGET_GONK)
+ // In gonk, the master b2g process sets its umask to 0027 because
+ // there's no reason for it to ever create world-readable files.
+ // The updater binary, however, needs to do this, and it inherits
+ // the master process's cautious umask. So we drop down a bit here.
+ umask(0022);
+
+ // Remount the /system partition as read-write for gonk. The destructor will
+ // remount /system as read-only. We add an extra level of scope here to avoid
+ // calling LogFinish() before the GonkAutoMounter destructor has a chance
+ // to be called
+ {
+#if !defined(TEST_UPDATER)
+ GonkAutoMounter mounter;
+ if (mounter.GetAccess() != MountAccess::ReadWrite) {
+ WriteStatusFile(FILESYSTEM_MOUNT_READWRITE_ERROR);
+ return 1;
+ }
+#endif
+#endif
+
+ if (sStagedUpdate) {
+ // When staging updates, blow away the old installation directory and create
+ // it from scratch.
+ ensure_remove_recursive(gWorkingDirPath);
+ }
+ if (!sReplaceRequest) {
+ // Try to create the destination directory if it doesn't exist
+ int rv = NS_tmkdir(gWorkingDirPath, 0755);
+ if (rv != OK && errno != EEXIST) {
+#ifdef XP_MACOSX
+ if (isElevated) {
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(true);
+ }
+#endif
+ return 1;
+ }
+ }
+
+#ifdef XP_WIN
+ // For replace requests, we don't need to do any real updates, so this is not
+ // necessary.
+ if (!sReplaceRequest) {
+ // Allocate enough space for the length of the path an optional additional
+ // trailing slash and null termination.
+ NS_tchar *destpath = (NS_tchar *) malloc((NS_tstrlen(gWorkingDirPath) + 2) * sizeof(NS_tchar));
+ if (!destpath) {
+ return 1;
+ }
+
+ NS_tchar *c = destpath;
+ NS_tstrcpy(c, gWorkingDirPath);
+ c += NS_tstrlen(gWorkingDirPath);
+ if (gWorkingDirPath[NS_tstrlen(gWorkingDirPath) - 1] != NS_T('/') &&
+ gWorkingDirPath[NS_tstrlen(gWorkingDirPath) - 1] != NS_T('\\')) {
+ NS_tstrcat(c, NS_T("/"));
+ c += NS_tstrlen(NS_T("/"));
+ }
+ *c = NS_T('\0');
+ c++;
+
+ gDestPath = destpath;
+ }
+
+ NS_tchar applyDirLongPath[MAXPATHLEN];
+ if (!GetLongPathNameW(gWorkingDirPath, applyDirLongPath,
+ sizeof(applyDirLongPath) / sizeof(applyDirLongPath[0]))) {
+ LOG(("NS_main: unable to find apply to dir: " LOG_S, gWorkingDirPath));
+ LogFinish();
+ WriteStatusFile(WRITE_ERROR_APPLY_DIR_PATH);
+ EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ if (argc > callbackIndex) {
+ LaunchCallbackApp(argv[5], argc - callbackIndex,
+ argv + callbackIndex, sUsingService);
+ }
+ return 1;
+ }
+
+ HANDLE callbackFile = INVALID_HANDLE_VALUE;
+ if (argc > callbackIndex) {
+ // If the callback executable is specified it must exist for a successful
+ // update. It is important we null out the whole buffer here because later
+ // we make the assumption that the callback application is inside the
+ // apply-to dir. If we don't have a fully null'ed out buffer it can lead
+ // to stack corruption which causes crashes and other problems.
+ NS_tchar callbackLongPath[MAXPATHLEN];
+ ZeroMemory(callbackLongPath, sizeof(callbackLongPath));
+ NS_tchar *targetPath = argv[callbackIndex];
+ NS_tchar buffer[MAXPATHLEN * 2] = { NS_T('\0') };
+ size_t bufferLeft = MAXPATHLEN * 2;
+ if (sReplaceRequest) {
+ // In case of replace requests, we should look for the callback file in
+ // the destination directory.
+ size_t commonPrefixLength = PathCommonPrefixW(argv[callbackIndex],
+ gInstallDirPath,
+ nullptr);
+ NS_tchar *p = buffer;
+ NS_tstrncpy(p, argv[callbackIndex], commonPrefixLength);
+ p += commonPrefixLength;
+ bufferLeft -= commonPrefixLength;
+ NS_tstrncpy(p, gInstallDirPath + commonPrefixLength, bufferLeft);
+
+ size_t len = NS_tstrlen(gInstallDirPath + commonPrefixLength);
+ p += len;
+ bufferLeft -= len;
+ *p = NS_T('\\');
+ ++p;
+ bufferLeft--;
+ *p = NS_T('\0');
+ NS_tchar installDir[MAXPATHLEN];
+ NS_tstrcpy(installDir, gInstallDirPath);
+ size_t callbackPrefixLength = PathCommonPrefixW(argv[callbackIndex],
+ installDir,
+ nullptr);
+ NS_tstrncpy(p, argv[callbackIndex] + std::max(callbackPrefixLength,
+ commonPrefixLength), bufferLeft);
+ targetPath = buffer;
+ }
+ if (!GetLongPathNameW(targetPath, callbackLongPath,
+ sizeof(callbackLongPath)/sizeof(callbackLongPath[0]))) {
+ LOG(("NS_main: unable to find callback file: " LOG_S, targetPath));
+ LogFinish();
+ WriteStatusFile(WRITE_ERROR_CALLBACK_PATH);
+ EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ if (argc > callbackIndex) {
+ LaunchCallbackApp(argv[5],
+ argc - callbackIndex,
+ argv + callbackIndex,
+ sUsingService);
+ }
+ return 1;
+ }
+
+ // Doing this is only necessary when we're actually applying a patch.
+ if (!sReplaceRequest) {
+ int len = NS_tstrlen(applyDirLongPath);
+ NS_tchar *s = callbackLongPath;
+ NS_tchar *d = gCallbackRelPath;
+ // advance to the apply to directory and advance past the trailing backslash
+ // if present.
+ s += len;
+ if (*s == NS_T('\\'))
+ ++s;
+
+ // Copy the string and replace backslashes with forward slashes along the
+ // way.
+ do {
+ if (*s == NS_T('\\'))
+ *d = NS_T('/');
+ else
+ *d = *s;
+ ++s;
+ ++d;
+ } while (*s);
+ *d = NS_T('\0');
+ ++d;
+
+ const size_t callbackBackupPathBufSize =
+ sizeof(gCallbackBackupPath)/sizeof(gCallbackBackupPath[0]);
+ const int callbackBackupPathLen =
+ NS_tsnprintf(gCallbackBackupPath, callbackBackupPathBufSize,
+ NS_T("%s" CALLBACK_BACKUP_EXT), argv[callbackIndex]);
+
+ if (callbackBackupPathLen < 0 ||
+ callbackBackupPathLen >= static_cast<int>(callbackBackupPathBufSize)) {
+ LOG(("NS_main: callback backup path truncated"));
+ LogFinish();
+ WriteStatusFile(USAGE_ERROR);
+
+ // Don't attempt to launch the callback when the callback path is
+ // longer than expected.
+ EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ return 1;
+ }
+
+ // Make a copy of the callback executable so it can be read when patching.
+ NS_tremove(gCallbackBackupPath);
+ if(!CopyFileW(argv[callbackIndex], gCallbackBackupPath, true)) {
+ DWORD copyFileError = GetLastError();
+ LOG(("NS_main: failed to copy callback file " LOG_S
+ " into place at " LOG_S, argv[callbackIndex], gCallbackBackupPath));
+ LogFinish();
+ if (copyFileError == ERROR_ACCESS_DENIED) {
+ WriteStatusFile(WRITE_ERROR_ACCESS_DENIED);
+ } else {
+ WriteStatusFile(WRITE_ERROR_CALLBACK_APP);
+ }
+
+ EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ LaunchCallbackApp(argv[callbackIndex],
+ argc - callbackIndex,
+ argv + callbackIndex,
+ sUsingService);
+ return 1;
+ }
+
+ // Since the process may be signaled as exited by WaitForSingleObject before
+ // the release of the executable image try to lock the main executable file
+ // multiple times before giving up. If we end up giving up, we won't
+ // fail the update.
+ const int max_retries = 10;
+ int retries = 1;
+ DWORD lastWriteError = 0;
+ do {
+ // By opening a file handle wihout FILE_SHARE_READ to the callback
+ // executable, the OS will prevent launching the process while it is
+ // being updated.
+ callbackFile = CreateFileW(targetPath,
+ DELETE | GENERIC_WRITE,
+ // allow delete, rename, and write
+ FILE_SHARE_DELETE | FILE_SHARE_WRITE,
+ nullptr, OPEN_EXISTING, 0, nullptr);
+ if (callbackFile != INVALID_HANDLE_VALUE)
+ break;
+
+ lastWriteError = GetLastError();
+ LOG(("NS_main: callback app file open attempt %d failed. " \
+ "File: " LOG_S ". Last error: %d", retries,
+ targetPath, lastWriteError));
+
+ Sleep(100);
+ } while (++retries <= max_retries);
+
+ // CreateFileW will fail if the callback executable is already in use.
+ if (callbackFile == INVALID_HANDLE_VALUE) {
+ // Only fail the update if the last error was not a sharing violation.
+ if (lastWriteError != ERROR_SHARING_VIOLATION) {
+ LOG(("NS_main: callback app file in use, failed to exclusively open " \
+ "executable file: " LOG_S, argv[callbackIndex]));
+ LogFinish();
+ if (lastWriteError == ERROR_ACCESS_DENIED) {
+ WriteStatusFile(WRITE_ERROR_ACCESS_DENIED);
+ } else {
+ WriteStatusFile(WRITE_ERROR_CALLBACK_APP);
+ }
+
+ NS_tremove(gCallbackBackupPath);
+ EXIT_WHEN_ELEVATED(elevatedLockFilePath, updateLockFileHandle, 1);
+ LaunchCallbackApp(argv[5],
+ argc - callbackIndex,
+ argv + callbackIndex,
+ sUsingService);
+ return 1;
+ }
+ LOG(("NS_main: callback app file in use, continuing without " \
+ "exclusive access for executable file: " LOG_S,
+ argv[callbackIndex]));
+ }
+ }
+ }
+
+ // DELETE_DIR is not required when performing a staged update or replace
+ // request; it can be used during a replace request but then it doesn't
+ // use gDeleteDirPath.
+ if (!sStagedUpdate && !sReplaceRequest) {
+ // The directory to move files that are in use to on Windows. This directory
+ // will be deleted after the update is finished, on OS reboot using
+ // MoveFileEx if it contains files that are in use, or by the post update
+ // process after the update finishes. On Windows when performing a normal
+ // update (e.g. the update is not a staged update and is not a replace
+ // request) gWorkingDirPath is the same as gInstallDirPath and
+ // gWorkingDirPath is used because it is the destination directory.
+ NS_tsnprintf(gDeleteDirPath,
+ sizeof(gDeleteDirPath) / sizeof(gDeleteDirPath[0]),
+ NS_T("%s/%s"), gWorkingDirPath, DELETE_DIR);
+
+ if (NS_taccess(gDeleteDirPath, F_OK)) {
+ NS_tmkdir(gDeleteDirPath, 0755);
+ }
+ }
+#endif /* XP_WIN */
+
+ // Run update process on a background thread. ShowProgressUI may return
+ // before QuitProgressUI has been called, so wait for UpdateThreadFunc to
+ // terminate. Avoid showing the progress UI when staging an update, or if this
+ // is an elevated process on OSX.
+ Thread t;
+ if (t.Run(UpdateThreadFunc, nullptr) == 0) {
+ if (!sStagedUpdate && !sReplaceRequest
+#ifdef XP_MACOSX
+ && !isElevated
+#endif
+ ) {
+ ShowProgressUI();
+ }
+ }
+ t.Join();
+
+#ifdef XP_WIN
+ if (argc > callbackIndex && !sReplaceRequest) {
+ if (callbackFile != INVALID_HANDLE_VALUE) {
+ CloseHandle(callbackFile);
+ }
+ // Remove the copy of the callback executable.
+ NS_tremove(gCallbackBackupPath);
+ }
+
+ if (!sStagedUpdate && !sReplaceRequest && _wrmdir(gDeleteDirPath)) {
+ LOG(("NS_main: unable to remove directory: " LOG_S ", err: %d",
+ DELETE_DIR, errno));
+ // The directory probably couldn't be removed due to it containing files
+ // that are in use and will be removed on OS reboot. The call to remove the
+ // directory on OS reboot is done after the calls to remove the files so the
+ // files are removed first on OS reboot since the directory must be empty
+ // for the directory removal to be successful. The MoveFileEx call to remove
+ // the directory on OS reboot will fail if the process doesn't have write
+ // access to the HKEY_LOCAL_MACHINE registry key but this is ok since the
+ // installer / uninstaller will delete the directory along with its contents
+ // after an update is applied, on reinstall, and on uninstall.
+ if (MoveFileEx(gDeleteDirPath, nullptr, MOVEFILE_DELAY_UNTIL_REBOOT)) {
+ LOG(("NS_main: directory will be removed on OS reboot: " LOG_S,
+ DELETE_DIR));
+ } else {
+ LOG(("NS_main: failed to schedule OS reboot removal of " \
+ "directory: " LOG_S, DELETE_DIR));
+ }
+ }
+#endif /* XP_WIN */
+
+#if defined(MOZ_WIDGET_GONK)
+ } // end the extra level of scope for the GonkAutoMounter
+#endif
+
+#ifdef XP_MACOSX
+ // When the update is successful remove the precomplete file in the root of
+ // the application bundle and move the distribution directory from
+ // Contents/MacOS to Contents/Resources and if both exist delete the
+ // directory under Contents/MacOS (see Bug 1068439).
+ if (gSucceeded && !sStagedUpdate) {
+ NS_tchar oldPrecomplete[MAXPATHLEN];
+ NS_tsnprintf(oldPrecomplete, sizeof(oldPrecomplete)/sizeof(oldPrecomplete[0]),
+ NS_T("%s/precomplete"), gInstallDirPath);
+ NS_tremove(oldPrecomplete);
+
+ NS_tchar oldDistDir[MAXPATHLEN];
+ NS_tsnprintf(oldDistDir, sizeof(oldDistDir)/sizeof(oldDistDir[0]),
+ NS_T("%s/Contents/MacOS/distribution"), gInstallDirPath);
+ int rv = NS_taccess(oldDistDir, F_OK);
+ if (!rv) {
+ NS_tchar newDistDir[MAXPATHLEN];
+ NS_tsnprintf(newDistDir, sizeof(newDistDir)/sizeof(newDistDir[0]),
+ NS_T("%s/Contents/Resources/distribution"), gInstallDirPath);
+ rv = NS_taccess(newDistDir, F_OK);
+ if (!rv) {
+ LOG(("New distribution directory already exists... removing old " \
+ "distribution directory: " LOG_S, oldDistDir));
+ rv = ensure_remove_recursive(oldDistDir);
+ if (rv) {
+ LOG(("Removing old distribution directory failed - err: %d", rv));
+ }
+ } else {
+ LOG(("Moving old distribution directory to new location. src: " LOG_S \
+ ", dst:" LOG_S, oldDistDir, newDistDir));
+ rv = rename_file(oldDistDir, newDistDir, true);
+ if (rv) {
+ LOG(("Moving old distribution directory to new location failed - " \
+ "err: %d", rv));
+ }
+ }
+ }
+ }
+
+ if (isElevated) {
+ SetGroupOwnershipAndPermissions(gInstallDirPath);
+ freeArguments(argc, argv);
+ CleanupElevatedMacUpdate(false);
+ } else if (IsOwnedByGroupAdmin(gInstallDirPath)) {
+ // If the group ownership of the Firefox .app bundle was set to the "admin"
+ // group during a previous elevated update, we need to ensure that all files
+ // in the bundle have group ownership of "admin" as well as write permission
+ // for the group to not break updates in the future.
+ SetGroupOwnershipAndPermissions(gInstallDirPath);
+ }
+#endif /* XP_MACOSX */
+
+ LogFinish();
+
+ int retVal = LaunchCallbackAndPostProcessApps(argc, argv, callbackIndex
+#ifdef XP_WIN
+ , elevatedLockFilePath
+ , updateLockFileHandle
+#elif XP_MACOSX
+ , isElevated
+#endif
+ );
+
+ return retVal ? retVal : (gSucceeded ? 0 : 1);
+}
+
+class ActionList
+{
+public:
+ ActionList() : mFirst(nullptr), mLast(nullptr), mCount(0) { }
+ ~ActionList();
+
+ void Append(Action* action);
+ int Prepare();
+ int Execute();
+ void Finish(int status);
+
+private:
+ Action *mFirst;
+ Action *mLast;
+ int mCount;
+};
+
+ActionList::~ActionList()
+{
+ Action* a = mFirst;
+ while (a) {
+ Action *b = a;
+ a = a->mNext;
+ delete b;
+ }
+}
+
+void
+ActionList::Append(Action *action)
+{
+ if (mLast)
+ mLast->mNext = action;
+ else
+ mFirst = action;
+
+ mLast = action;
+ mCount++;
+}
+
+int
+ActionList::Prepare()
+{
+ // If the action list is empty then we should fail in order to signal that
+ // something has gone wrong. Otherwise we report success when nothing is
+ // actually done. See bug 327140.
+ if (mCount == 0) {
+ LOG(("empty action list"));
+ return MAR_ERROR_EMPTY_ACTION_LIST;
+ }
+
+ Action *a = mFirst;
+ int i = 0;
+ while (a) {
+ int rv = a->Prepare();
+ if (rv)
+ return rv;
+
+ float percent = float(++i) / float(mCount);
+ UpdateProgressUI(PROGRESS_PREPARE_SIZE * percent);
+
+ a = a->mNext;
+ }
+
+ return OK;
+}
+
+int
+ActionList::Execute()
+{
+ int currentProgress = 0, maxProgress = 0;
+ Action *a = mFirst;
+ while (a) {
+ maxProgress += a->mProgressCost;
+ a = a->mNext;
+ }
+
+ a = mFirst;
+ while (a) {
+ int rv = a->Execute();
+ if (rv) {
+ LOG(("### execution failed"));
+ return rv;
+ }
+
+ currentProgress += a->mProgressCost;
+ float percent = float(currentProgress) / float(maxProgress);
+ UpdateProgressUI(PROGRESS_PREPARE_SIZE +
+ PROGRESS_EXECUTE_SIZE * percent);
+
+ a = a->mNext;
+ }
+
+ return OK;
+}
+
+void
+ActionList::Finish(int status)
+{
+ Action *a = mFirst;
+ int i = 0;
+ while (a) {
+ a->Finish(status);
+
+ float percent = float(++i) / float(mCount);
+ UpdateProgressUI(PROGRESS_PREPARE_SIZE +
+ PROGRESS_EXECUTE_SIZE +
+ PROGRESS_FINISH_SIZE * percent);
+
+ a = a->mNext;
+ }
+
+ if (status == OK)
+ gSucceeded = true;
+}
+
+
+#ifdef XP_WIN
+int add_dir_entries(const NS_tchar *dirpath, ActionList *list)
+{
+ int rv = OK;
+ WIN32_FIND_DATAW finddata;
+ HANDLE hFindFile;
+ NS_tchar searchspec[MAXPATHLEN];
+ NS_tchar foundpath[MAXPATHLEN];
+
+ NS_tsnprintf(searchspec, sizeof(searchspec)/sizeof(searchspec[0]),
+ NS_T("%s*"), dirpath);
+ mozilla::UniquePtr<const NS_tchar> pszSpec(get_full_path(searchspec));
+
+ hFindFile = FindFirstFileW(pszSpec.get(), &finddata);
+ if (hFindFile != INVALID_HANDLE_VALUE) {
+ do {
+ // Don't process the current or parent directory.
+ if (NS_tstrcmp(finddata.cFileName, NS_T(".")) == 0 ||
+ NS_tstrcmp(finddata.cFileName, NS_T("..")) == 0)
+ continue;
+
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s%s"), dirpath, finddata.cFileName);
+ if (finddata.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) {
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s/"), foundpath);
+ // Recurse into the directory.
+ rv = add_dir_entries(foundpath, list);
+ if (rv) {
+ LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv));
+ return rv;
+ }
+ } else {
+ // Add the file to be removed to the ActionList.
+ NS_tchar *quotedpath = get_quoted_path(foundpath);
+ if (!quotedpath)
+ return PARSE_ERROR;
+
+ Action *action = new RemoveFile();
+ rv = action->Parse(quotedpath);
+ if (rv) {
+ LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d",
+ quotedpath, rv));
+ return rv;
+ }
+ free(quotedpath);
+
+ list->Append(action);
+ }
+ } while (FindNextFileW(hFindFile, &finddata) != 0);
+
+ FindClose(hFindFile);
+ {
+ // Add the directory to be removed to the ActionList.
+ NS_tchar *quotedpath = get_quoted_path(dirpath);
+ if (!quotedpath)
+ return PARSE_ERROR;
+
+ Action *action = new RemoveDir();
+ rv = action->Parse(quotedpath);
+ if (rv) {
+ LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d",
+ quotedpath, rv));
+ } else {
+ list->Append(action);
+ }
+ free(quotedpath);
+ }
+ }
+
+ return rv;
+}
+
+#elif defined(SOLARIS)
+int add_dir_entries(const NS_tchar *dirpath, ActionList *list)
+{
+ int rv = OK;
+ NS_tchar foundpath[MAXPATHLEN];
+ struct {
+ dirent dent_buffer;
+ char chars[MAXNAMLEN];
+ } ent_buf;
+ struct dirent* ent;
+ mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath));
+
+ DIR* dir = opendir(searchpath.get());
+ if (!dir) {
+ LOG(("add_dir_entries error on opendir: " LOG_S ", err: %d", searchpath.get(),
+ errno));
+ return UNEXPECTED_FILE_OPERATION_ERROR;
+ }
+
+ while (readdir_r(dir, (dirent *)&ent_buf, &ent) == 0 && ent) {
+ if ((strcmp(ent->d_name, ".") == 0) ||
+ (strcmp(ent->d_name, "..") == 0))
+ continue;
+
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s%s"), searchpath.get(), ent->d_name);
+ struct stat64 st_buf;
+ int test = stat64(foundpath, &st_buf);
+ if (test) {
+ closedir(dir);
+ return UNEXPECTED_FILE_OPERATION_ERROR;
+ }
+ if (S_ISDIR(st_buf.st_mode)) {
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s/"), foundpath);
+ // Recurse into the directory.
+ rv = add_dir_entries(foundpath, list);
+ if (rv) {
+ LOG(("add_dir_entries error: " LOG_S ", err: %d", foundpath, rv));
+ closedir(dir);
+ return rv;
+ }
+ } else {
+ // Add the file to be removed to the ActionList.
+ NS_tchar *quotedpath = get_quoted_path(get_relative_path(foundpath));
+ if (!quotedpath) {
+ closedir(dir);
+ return PARSE_ERROR;
+ }
+
+ Action *action = new RemoveFile();
+ rv = action->Parse(quotedpath);
+ if (rv) {
+ LOG(("add_dir_entries Parse error on recurse: " LOG_S ", err: %d",
+ quotedpath, rv));
+ closedir(dir);
+ return rv;
+ }
+
+ list->Append(action);
+ }
+ }
+ closedir(dir);
+
+ // Add the directory to be removed to the ActionList.
+ NS_tchar *quotedpath = get_quoted_path(get_relative_path(dirpath));
+ if (!quotedpath)
+ return PARSE_ERROR;
+
+ Action *action = new RemoveDir();
+ rv = action->Parse(quotedpath);
+ if (rv) {
+ LOG(("add_dir_entries Parse error on close: " LOG_S ", err: %d",
+ quotedpath, rv));
+ }
+ else {
+ list->Append(action);
+ }
+
+ return rv;
+}
+
+#else
+
+int add_dir_entries(const NS_tchar *dirpath, ActionList *list)
+{
+ int rv = OK;
+ FTS *ftsdir;
+ FTSENT *ftsdirEntry;
+ mozilla::UniquePtr<NS_tchar[]> searchpath(get_full_path(dirpath));
+
+ // Remove the trailing slash so the paths don't contain double slashes. The
+ // existence of the slash has already been checked in DoUpdate.
+ searchpath[NS_tstrlen(searchpath.get()) - 1] = NS_T('\0');
+ char* const pathargv[] = {searchpath.get(), nullptr};
+
+ // FTS_NOCHDIR is used so relative paths from the destination directory are
+ // returned.
+ if (!(ftsdir = fts_open(pathargv,
+ FTS_PHYSICAL | FTS_NOSTAT | FTS_XDEV | FTS_NOCHDIR,
+ nullptr)))
+ return UNEXPECTED_FILE_OPERATION_ERROR;
+
+ while ((ftsdirEntry = fts_read(ftsdir)) != nullptr) {
+ NS_tchar foundpath[MAXPATHLEN];
+ NS_tchar *quotedpath = nullptr;
+ Action *action = nullptr;
+
+ switch (ftsdirEntry->fts_info) {
+ // Filesystem objects that shouldn't be in the application's directories
+ case FTS_SL:
+ case FTS_SLNONE:
+ case FTS_DEFAULT:
+ LOG(("add_dir_entries: found a non-standard file: " LOG_S,
+ ftsdirEntry->fts_path));
+ // Fall through and try to remove as a file
+ MOZ_FALLTHROUGH;
+
+ // Files
+ case FTS_F:
+ case FTS_NSOK:
+ // Add the file to be removed to the ActionList.
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s"), ftsdirEntry->fts_accpath);
+ quotedpath = get_quoted_path(get_relative_path(foundpath));
+ if (!quotedpath) {
+ rv = UPDATER_QUOTED_PATH_MEM_ERROR;
+ break;
+ }
+ action = new RemoveFile();
+ rv = action->Parse(quotedpath);
+ free(quotedpath);
+ if (!rv)
+ list->Append(action);
+ break;
+
+ // Directories
+ case FTS_DP:
+ rv = OK;
+ // Add the directory to be removed to the ActionList.
+ NS_tsnprintf(foundpath, sizeof(foundpath)/sizeof(foundpath[0]),
+ NS_T("%s/"), ftsdirEntry->fts_accpath);
+ quotedpath = get_quoted_path(get_relative_path(foundpath));
+ if (!quotedpath) {
+ rv = UPDATER_QUOTED_PATH_MEM_ERROR;
+ break;
+ }
+
+ action = new RemoveDir();
+ rv = action->Parse(quotedpath);
+ free(quotedpath);
+ if (!rv)
+ list->Append(action);
+ break;
+
+ // Errors
+ case FTS_DNR:
+ case FTS_NS:
+ // ENOENT is an acceptable error for FTS_DNR and FTS_NS and means that
+ // we're racing with ourselves. Though strange, the entry will be
+ // removed anyway.
+ if (ENOENT == ftsdirEntry->fts_errno) {
+ rv = OK;
+ break;
+ }
+ MOZ_FALLTHROUGH;
+
+ case FTS_ERR:
+ rv = UNEXPECTED_FILE_OPERATION_ERROR;
+ LOG(("add_dir_entries: fts_read() error: " LOG_S ", err: %d",
+ ftsdirEntry->fts_path, ftsdirEntry->fts_errno));
+ break;
+
+ case FTS_DC:
+ rv = UNEXPECTED_FILE_OPERATION_ERROR;
+ LOG(("add_dir_entries: fts_read() returned FT_DC: " LOG_S,
+ ftsdirEntry->fts_path));
+ break;
+
+ default:
+ // FTS_D is ignored and FTS_DP is used instead (post-order).
+ rv = OK;
+ break;
+ }
+
+ if (rv != OK)
+ break;
+ }
+
+ fts_close(ftsdir);
+
+ return rv;
+}
+#endif
+
+static NS_tchar*
+GetManifestContents(const NS_tchar *manifest)
+{
+ AutoFile mfile(NS_tfopen(manifest, NS_T("rb")));
+ if (mfile == nullptr) {
+ LOG(("GetManifestContents: error opening manifest file: " LOG_S, manifest));
+ return nullptr;
+ }
+
+ struct stat ms;
+ int rv = fstat(fileno((FILE *)mfile), &ms);
+ if (rv) {
+ LOG(("GetManifestContents: error stating manifest file: " LOG_S, manifest));
+ return nullptr;
+ }
+
+ char *mbuf = (char *) malloc(ms.st_size + 1);
+ if (!mbuf)
+ return nullptr;
+
+ size_t r = ms.st_size;
+ char *rb = mbuf;
+ while (r) {
+ const size_t count = mmin(SSIZE_MAX, r);
+ size_t c = fread(rb, 1, count, mfile);
+ if (c != count) {
+ LOG(("GetManifestContents: error reading manifest file: " LOG_S, manifest));
+ free(mbuf);
+ return nullptr;
+ }
+
+ r -= c;
+ rb += c;
+ }
+ mbuf[ms.st_size] = '\0';
+ rb = mbuf;
+
+#ifndef XP_WIN
+ return rb;
+#else
+ NS_tchar *wrb = (NS_tchar *) malloc((ms.st_size + 1) * sizeof(NS_tchar));
+ if (!wrb) {
+ free(mbuf);
+ return nullptr;
+ }
+
+ if (!MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, rb, -1, wrb,
+ ms.st_size + 1)) {
+ LOG(("GetManifestContents: error converting utf8 to utf16le: %d", GetLastError()));
+ free(mbuf);
+ free(wrb);
+ return nullptr;
+ }
+ free(mbuf);
+
+ return wrb;
+#endif
+}
+
+int AddPreCompleteActions(ActionList *list)
+{
+ if (sIsOSUpdate) {
+ return OK;
+ }
+
+#ifdef XP_MACOSX
+ mozilla::UniquePtr<NS_tchar[]> manifestPath(get_full_path(
+ NS_T("Contents/Resources/precomplete")));
+#else
+ mozilla::UniquePtr<NS_tchar[]> manifestPath(get_full_path(
+ NS_T("precomplete")));
+#endif
+
+ NS_tchar *rb = GetManifestContents(manifestPath.get());
+ if (rb == nullptr) {
+ LOG(("AddPreCompleteActions: error getting contents of precomplete " \
+ "manifest"));
+ // Applications aren't required to have a precomplete manifest. The mar
+ // generation scripts enforce the presence of a precomplete manifest.
+ return OK;
+ }
+
+ int rv;
+ NS_tchar *line;
+ while((line = mstrtok(kNL, &rb)) != 0) {
+ // skip comments
+ if (*line == NS_T('#'))
+ continue;
+
+ NS_tchar *token = mstrtok(kWhitespace, &line);
+ if (!token) {
+ LOG(("AddPreCompleteActions: token not found in manifest"));
+ return PARSE_ERROR;
+ }
+
+ Action *action = nullptr;
+ if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file
+ action = new RemoveFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("remove-cc")) == 0) { // no longer supported
+ continue;
+ }
+ else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty
+ action = new RemoveDir();
+ }
+ else {
+ LOG(("AddPreCompleteActions: unknown token: " LOG_S, token));
+ return PARSE_ERROR;
+ }
+
+ if (!action)
+ return BAD_ACTION_ERROR;
+
+ rv = action->Parse(line);
+ if (rv)
+ return rv;
+
+ list->Append(action);
+ }
+
+ return OK;
+}
+
+int DoUpdate()
+{
+ NS_tchar manifest[MAXPATHLEN];
+ NS_tsnprintf(manifest, sizeof(manifest)/sizeof(manifest[0]),
+ NS_T("%s/updating/update.manifest"), gWorkingDirPath);
+ ensure_parent_dir(manifest);
+
+ // extract the manifest
+ int rv = gArchiveReader.ExtractFile("updatev3.manifest", manifest);
+ if (rv) {
+ rv = gArchiveReader.ExtractFile("updatev2.manifest", manifest);
+ if (rv) {
+ LOG(("DoUpdate: error extracting manifest file"));
+ return rv;
+ }
+ }
+
+ NS_tchar *rb = GetManifestContents(manifest);
+ NS_tremove(manifest);
+ if (rb == nullptr) {
+ LOG(("DoUpdate: error opening manifest file: " LOG_S, manifest));
+ return READ_ERROR;
+ }
+
+
+ ActionList list;
+ NS_tchar *line;
+ bool isFirstAction = true;
+
+ while((line = mstrtok(kNL, &rb)) != 0) {
+ // skip comments
+ if (*line == NS_T('#'))
+ continue;
+
+ NS_tchar *token = mstrtok(kWhitespace, &line);
+ if (!token) {
+ LOG(("DoUpdate: token not found in manifest"));
+ return PARSE_ERROR;
+ }
+
+ if (isFirstAction) {
+ isFirstAction = false;
+ // The update manifest isn't required to have a type declaration. The mar
+ // generation scripts enforce the presence of the type declaration.
+ if (NS_tstrcmp(token, NS_T("type")) == 0) {
+ const NS_tchar *type = mstrtok(kQuote, &line);
+ LOG(("UPDATE TYPE " LOG_S, type));
+ if (NS_tstrcmp(type, NS_T("complete")) == 0) {
+ rv = AddPreCompleteActions(&list);
+ if (rv)
+ return rv;
+ }
+ continue;
+ }
+ }
+
+ Action *action = nullptr;
+ if (NS_tstrcmp(token, NS_T("remove")) == 0) { // rm file
+ action = new RemoveFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("rmdir")) == 0) { // rmdir if empty
+ action = new RemoveDir();
+ }
+ else if (NS_tstrcmp(token, NS_T("rmrfdir")) == 0) { // rmdir recursive
+ const NS_tchar *reldirpath = mstrtok(kQuote, &line);
+ if (!reldirpath)
+ return PARSE_ERROR;
+
+ if (reldirpath[NS_tstrlen(reldirpath) - 1] != NS_T('/'))
+ return PARSE_ERROR;
+
+ rv = add_dir_entries(reldirpath, &list);
+ if (rv)
+ return rv;
+
+ continue;
+ }
+ else if (NS_tstrcmp(token, NS_T("add")) == 0) {
+ action = new AddFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("patch")) == 0) {
+ action = new PatchFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("add-if")) == 0) { // Add if exists
+ action = new AddIfFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("add-if-not")) == 0) { // Add if not exists
+ action = new AddIfNotFile();
+ }
+ else if (NS_tstrcmp(token, NS_T("patch-if")) == 0) { // Patch if exists
+ action = new PatchIfFile();
+ }
+ else {
+ LOG(("DoUpdate: unknown token: " LOG_S, token));
+ return PARSE_ERROR;
+ }
+
+ if (!action)
+ return BAD_ACTION_ERROR;
+
+ rv = action->Parse(line);
+ if (rv)
+ return rv;
+
+ list.Append(action);
+ }
+
+ rv = list.Prepare();
+ if (rv)
+ return rv;
+
+ rv = list.Execute();
+
+ list.Finish(rv);
+ return rv;
+}
diff --git a/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest
new file mode 100644
index 000000000..9a6cdb565
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.exe.comctl32.manifest
@@ -0,0 +1,38 @@
+<?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="Updater"
+ type="win32"
+/>
+<description>Updater</description>
+<dependency>
+ <dependentAssembly>
+ <assemblyIdentity
+ type="win32"
+ name="Microsoft.Windows.Common-Controls"
+ version="6.0.0.0"
+ processorArchitecture="*"
+ publicKeyToken="6595b64144ccf1df"
+ language="*"
+ />
+ </dependentAssembly>
+</dependency>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:security>
+ <ms_asmv3:requestedPrivileges>
+ <ms_asmv3:requestedExecutionLevel level="asInvoker" 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}"/>
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/toolkit/mozapps/update/updater/updater.exe.manifest b/toolkit/mozapps/update/updater/updater.exe.manifest
new file mode 100644
index 000000000..cd229c954
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.exe.manifest
@@ -0,0 +1,26 @@
+<?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="Updater"
+ type="win32"
+/>
+<description>Updater</description>
+<ms_asmv3:trustInfo xmlns:ms_asmv3="urn:schemas-microsoft-com:asm.v3">
+ <ms_asmv3:security>
+ <ms_asmv3:requestedPrivileges>
+ <ms_asmv3:requestedExecutionLevel level="asInvoker" 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}"/>
+ <supportedOS Id="{e2011457-1546-43c5-a5fe-008deee3d3f0}"/>
+ </application>
+ </compatibility>
+</assembly>
diff --git a/toolkit/mozapps/update/updater/updater.ico b/toolkit/mozapps/update/updater/updater.ico
new file mode 100644
index 000000000..48457029d
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.ico
Binary files differ
diff --git a/toolkit/mozapps/update/updater/updater.png b/toolkit/mozapps/update/updater/updater.png
new file mode 100644
index 000000000..7b5e78907
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.png
Binary files differ
diff --git a/toolkit/mozapps/update/updater/updater.rc b/toolkit/mozapps/update/updater/updater.rc
new file mode 100644
index 000000000..7603eecb6
--- /dev/null
+++ b/toolkit/mozapps/update/updater/updater.rc
@@ -0,0 +1,137 @@
+/* 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.
+//
+#ifdef TEST_UPDATER
+#include "../resource.h"
+#define MANIFEST_PATH "../updater.exe.manifest"
+#define COMCTL32_MANIFEST_PATH "../updater.exe.comctl32.manifest"
+#define ICON_PATH "../updater.ico"
+#else
+#include "resource.h"
+#define MANIFEST_PATH "updater.exe.manifest"
+#define COMCTL32_MANIFEST_PATH "updater.exe.comctl32.manifest"
+#define ICON_PATH "updater.ico"
+#endif
+
+#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 MANIFEST_PATH
+IDR_COMCTL32_MANIFEST RT_MANIFEST COMCTL32_MANIFEST_PATH
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Icon
+//
+
+IDI_DIALOG ICON ICON_PATH
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Embedded an identifier to uniquely identiy this as a Mozilla updater.
+//
+
+STRINGTABLE
+{
+ IDS_UPDATER_IDENTITY, "moz-updater.exe-4cdccec4-5ee0-4a06-9817-4cd899a9db49"
+}
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// Dialog
+//
+
+IDD_DIALOG DIALOGEX 0, 0, 253, 41
+STYLE DS_SETFONT | DS_MODALFRAME | DS_FIXEDSYS | WS_POPUP | WS_CAPTION
+FONT 8, "MS Shell Dlg", 400, 0, 0x1
+BEGIN
+ CONTROL "",IDC_PROGRESS,"msctls_progress32",WS_BORDER,7,24,239,10
+ LTEXT "",IDC_INFO,7,8,239,13,SS_NOPREFIX
+END
+
+
+/////////////////////////////////////////////////////////////////////////////
+//
+// DESIGNINFO
+//
+
+#ifdef APSTUDIO_INVOKED
+GUIDELINES DESIGNINFO
+BEGIN
+ IDD_DIALOG, DIALOG
+ BEGIN
+ LEFTMARGIN, 7
+ RIGHTMARGIN, 246
+ TOPMARGIN, 7
+ BOTTOMMARGIN, 39
+ END
+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/mozapps/update/updater/win_dirent.cpp b/toolkit/mozapps/update/updater/win_dirent.cpp
new file mode 100644
index 000000000..b0807ba5e
--- /dev/null
+++ b/toolkit/mozapps/update/updater/win_dirent.cpp
@@ -0,0 +1,78 @@
+/* -*- Mode: C++; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et cindent: */
+/* 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 "win_dirent.h"
+#include <errno.h>
+#include <string.h>
+
+// This file implements the minimum set of dirent APIs used by updater.cpp on
+// Windows. If updater.cpp is modified to use more of this API, we need to
+// implement those parts here too.
+
+static dirent gDirEnt;
+
+DIR::DIR(const WCHAR* path)
+ : findHandle(INVALID_HANDLE_VALUE)
+{
+ memset(name, 0, sizeof(name));
+ wcsncpy(name, path, sizeof(name)/sizeof(name[0]));
+ wcsncat(name, L"\\*", sizeof(name)/sizeof(name[0]) - wcslen(name) - 1);
+}
+
+DIR::~DIR()
+{
+ if (findHandle != INVALID_HANDLE_VALUE) {
+ FindClose(findHandle);
+ }
+}
+
+dirent::dirent()
+{
+ d_name[0] = L'\0';
+}
+
+DIR*
+opendir(const WCHAR* path)
+{
+ return new DIR(path);
+}
+
+int
+closedir(DIR* dir)
+{
+ delete dir;
+ return 0;
+}
+
+dirent* readdir(DIR* dir)
+{
+ WIN32_FIND_DATAW data;
+ if (dir->findHandle != INVALID_HANDLE_VALUE) {
+ BOOL result = FindNextFileW(dir->findHandle, &data);
+ if (!result) {
+ if (GetLastError() != ERROR_FILE_NOT_FOUND) {
+ errno = ENOENT;
+ }
+ return 0;
+ }
+ } else {
+ // Reading the first directory entry
+ dir->findHandle = FindFirstFileW(dir->name, &data);
+ if (dir->findHandle == INVALID_HANDLE_VALUE) {
+ if (GetLastError() == ERROR_FILE_NOT_FOUND) {
+ errno = ENOENT;
+ } else {
+ errno = EBADF;
+ }
+ return 0;
+ }
+ }
+ memset(gDirEnt.d_name, 0, sizeof(gDirEnt.d_name));
+ wcsncpy(gDirEnt.d_name, data.cFileName,
+ sizeof(gDirEnt.d_name)/sizeof(gDirEnt.d_name[0]));
+ return &gDirEnt;
+}
+
diff --git a/toolkit/mozapps/update/updater/xpcshellCertificate.der b/toolkit/mozapps/update/updater/xpcshellCertificate.der
new file mode 100644
index 000000000..185b2dff4
--- /dev/null
+++ b/toolkit/mozapps/update/updater/xpcshellCertificate.der
Binary files differ